QwantiFi Credential Model
How your integrated application software authenticates to the QwantiFi broker, and how it keeps that access alive on its own without a person logging into a portal.
This is the "why it works this way" companion to the Integration Guide, which covers the topics, the message flow, and the endpoint mechanics. Read this page to understand the credential model you are integrating against; read that one to write the code.
The Big Picture
An integrated application instance talks to devices over the QwantiFi broker. To connect, it needs an MQTT credential. Unlike a physical device — which is issued a permanent username/password — an integrated application instance is issued a short-lived JSON Web Token (JWT) that it presents as its MQTT password.
Three ideas drive the whole design:
The platform is the credential authority. The broker does not mint, refresh, or revoke tokens — it only verifies them on connect. All issuance and refresh logic lives in the platform service.
Authentication and authorization are separate. The JWT answers "who are you?" The broker then answers "what may you do?" from a separate authorization store, looked up by the identity in the token. The token itself carries no permissions.
Identity is a scope, not a single terminal. A credential is keyed to a reseller (and optionally a customer/merchant), not to one physical lane. Every integrated application instance under the same scope shares one identity and one set of permissions.
Lifecycle at a Glance

The Integrated Application Identity
When your credential is provisioned, a stable username is derived from your scope:
Reseller-wide —
pos-{resellerId}, one identity covering every merchant under a reseller.Customer-scoped —
pos-{resellerId}-{customerId}, narrowed to a single merchant.
This username is the anchor for everything else: the JWT is issued for it, the broker's permission rules are keyed by it, and an audit record is stored under it. Because the identity is a scope, multiple integrated application terminals can run concurrently under the same username. Each must pick a distinct per-connection clientId so the broker does not treat them as the same session — the shared username is the identity, not the connection.
Authentication (the JWT)
The issued JWT is authentication-only: it asserts the username and carries an expiry, signed with HS256 using a secret the broker also holds. On connect, the broker checks the signature and the expiry — nothing else. The token deliberately contains no permission rules. Its lifetime is 8 hours by default and is configurable per environment, so drive your refresh timer off the expiresAt you were handed rather than a hard-coded number.
Authorization (The Permission Rules)
Separately, a small, least-privilege set of permission rules is written into the broker's authorization store, keyed by your integrated application username. These grant exactly two things, each as a single scope-wide pattern rather than a per-device list:
the topics you may publish to (commands toward devices in your scope), and
the topics you may subscribe to (responses coming back from those devices).
Because the grant is a scope-wide pattern, you are automatically authorized for any device that appears or disappears under your scope later — no per-device maintenance, and no re-provisioning when a device is added.
What You Receive
A successful provisioning hands you everything needed to connect — broker coordinates, your username, the JWT to use as the password, the topic patterns you are allowed to use, and the time the JWT expires plus a refresh token. The JWT itself is never stored on the platform side; only audit metadata is kept (who, what scope, when, expiry).
Refresh Tokens: Staying Alive Without the Portal
What Is A Refresh Token?
At onboarding, alongside the JWT, you receive a long-lived, opaque refresh token. It is a 256-bit high-entropy random string with no internal structure (not a JWT) and carrying no claims. Only a salted hash of it is stored on the platform side, never the token itself. The plaintext is returned to you exactly once, at issuance. This means a read of the platform's data store does not expose any usable refresh token; the platform can verify a token you present (by hashing it and comparing in constant time) but can never reproduce one.
If you lose it, it cannot be recovered — only re-issued through onboarding. Store it durably.
How Refresh Works
When your JWT is near expiry, call POST /api/pos-credentials/refresh with your username and refresh token. The presented token is hashed, compared against the stored hash, and if it matches and is still valid, a fresh JWT with a new expiry is minted and returned. Swap it in and reconnect.
An Up-front Second Factor: Your Current Access Token
Because the refresh endpoint sits on the public gateway, the refresh token is not the only thing standing between an attacker and a fresh credential. You must also present your current MQTT JWT as an Authorization: Bearer token on the refresh call. This proves the caller recently held a legitimately-issued credential, not merely a refresh token. Two properties make this practical:
Expired tokens are accepted here. The whole point of refresh is to renew a credential that is about to expire or has just expired, so the JWT's signature is validated but its expiry is deliberately ignored — on this endpoint only. Instead the token's age is bounded: a JWT issued more than 90 days ago is refused. Since the issued-at is re-stamped on every refresh, an actively-refreshing integrated application always passes, while an ancient leaked token does not.
The identity must match. The JWT's username claim must equal the username being refreshed, so a valid token for one scope cannot be used to refresh another.
Two Deliberate Properties
Refresh does not rotate the refresh token. The same refresh token keeps working for repeated refreshes, which is what lets an integrated application that was offline past its JWT's expiry come back on its own. The refresh response returns
"refreshToken": null— that is expected; keep the token you already hold and do not overwrite it with thenull.Re-onboarding does rotate it. If the portal re-provisions your scope, a brand-new refresh token is issued and the previous one stops working. Because the identity is shared per scope, this affects all integrated application instances under that scope.
Refreshing Also Keeps the Refresh Token Alive
The refresh token is issued with 90 days of life, and each successful refresh slides its expiry: if fewer than 60 days remain it is pushed back out to 60 days from now; if more remain it is left alone. The slide only ever extends — it never shortens a longer window, and it cannot revive an already-expired or revoked token. The practical effect: an integrated application that refreshes on schedule never falls below 60 days and never needs re-onboarding, while one that stays dark past that window lapses naturally.
When Refresh Stops Working
A refresh attempt is refused when any of these hold: the access-token factor is missing, malformed, wrongly-signed, older than 90 days, or for a different username; the username is unknown; the refresh token does not match; the refresh token has expired; or your scope has been revoked (§5). Two 401 responses are distinguishable, and it is worth branching on them:
{"error":"Unauthorized."}
The JWT factor failed.
Usually recoverable — refresh and retry once.
{"error":"Invalid username or refresh token."}
The refresh-token factor failed — wrong, expired, or revoked.
Not recoverable by retrying. Stop and surface a "needs re-onboarding" state.
What Your Integrated Application Is Responsible For
Persist the refresh token durably and atomically (write-then-rename), so a crash never leaves it half-written.
Refresh proactively, with margin — around 75–80% of the JWT lifetime is a good trigger. This is also what keeps the sliding window from lapsing.
Tolerate clock skew and retry transient failures (
502,503) with backoff.Keep one clean fallback: if a refresh is ever hard-rejected (expired or revoked), stop retrying and surface a "needs re-onboarding" state rather than hammering the endpoint.
Treat both secrets as passwords. Never log the JWT or the refresh token.
If Your Access Is Withdrawn
Access can be withdrawn by whoever issued your credentials — a decommissioned terminal, a merchant offboarding, or a credential believed compromised. This is an operator action on the issuing side; there is no endpoint for it on the integrated application-facing surface. What you would observe:
Refresh stops working, returning
{"error":"Invalid username or refresh token."}.Live connections are disconnected, and reconnecting is blocked — even with a JWT that has not expired yet. That block lifts on its own once the JWT would have expired anyway.
Publishes and subscribes stop being authorized, because the permission rules for your scope are removed.
Withdrawal comes in two flavors. The default is reversible: if it was done in error, access can be restored in place, and your integrated application simply refreshes with the original refresh token it still holds and reconnects — no re-onboarding. The stricter form is permanent, appropriate when a token may be compromised; restoring access then requires full re-onboarding through the portal. In either case, the correct client behavior is the same: stop retrying, surface the state, and contact whoever issued your credentials.
Design Rationale
Short-lived JWTs keep blast radius small and require no revocation list for the common case — tokens simply expire. Refresh tokens let an unattended integrated application stay alive without a human in the loop, while hash-only storage keeps a refresh-token leak from a database read off the table. Splitting authentication (the token) from authorization (the rules the broker consults live) is the single decision that makes instant withdrawal of access possible at all — and pairing it with a self-expiring connection block means it takes effect immediately without leaving lasting residue on a reusable scope identity. Sliding the refresh token's expiry on every use closes the last gap: an actively-running integrated application is never dragged back through a portal on the calendar's say-so, while a credential nobody is using still lapses on schedule.
Last updated

