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.
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.
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.
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.
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.
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).
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.
When your JWT is near expiry, call 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.
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.
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 the null.
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.
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.
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:
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.
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.
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.
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.
{"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.

These guides cover integrating a point-of-sale or backend application with QwantiFi, MagTek's cloud MQTT layer for commanding readers over the network. Read the credential model first to understand how your application authenticates to the broker and renews that access on its own, then follow the Integration Guide to connect, address devices by topic, and handle their responses.
New to QwantiFi? Start with the QwantiFi Overview under Services for the big picture.
Reference Section
Information Available
Explains how an integrated application authenticates to the QwantiFi broker with a short-lived token plus a long-lived refresh token, how it renews access without a portal login, and what happens when access is withdrawn.
Walks through connecting an integrated application to the broker and using QwantiFi topics to command one device or an entire fleet and handle their responses, with worked examples.
How an integrated application uses its QwantiFi MQTT credentials to talk to one device or many devices once those credentials have been distributed.
This guide is client-agnostic: it describes the MQTT concepts and message flow with pseudocode, so it applies to any MQTT client library (mqtt.js, MQTTnet, Paho, Eclipse Mosquitto clients, etc.).
A single integrated application credential lets one integrated application instance command every device it owns β there is no per-device credential to manage on the integrated application side.
A device has exactly one subscribe topic and one publish topic. It listens only for commands addressed to its own serial number and has no routing logic of its own.
A POS holds lists of publish and subscribe topics covering all the devices under its scope (a whole reseller, or a single merchant). It addresses each device by publishing to that device's topic and listens for each device's responses on a separate topic.
Commands and responses travel on different topic branches so a device never re-receives its own output and the integrated application is not fighting echo/recursion.
The mqttConfig object you were given at onboarding carries everything you need:
Example of a merchant-scoped credential (a reseller-wide credential looks the same but its username is pos-{reseller} and its topics span every merchant under that reseller). The scope is fixed at issue time β the integrated application does not select it:
The JWT is the password. Do not also send a separate password β the broker validates the JWT signature to authenticate you, then applies your authorization rules from its broker-side store, matched by your username.
Transport: trust port, not operatingMode. operatingMode is a coarse hint and is currently reported as mqtts regardless of environment. Decide TLS from the port you were issued and from what your onboarding contact told you the environment uses; if peerCommonName is populated, verify the server certificate against it.
Publish grant (what you may use): MagTek/13204/2026213/+/SendCommand
Actual command you publish (one device): MagTek/13204/2026213/TEST-001/SendCommand
Subscribe grant (what you may use): MagTek/13204/2026213/device/publish/+/MMSMessage and β¦/+/Status
The grant is leaf-specific: publishing is allowed only on the SendCommand leaf, and subscribing only on the MMSMessage and Status leaves β a broad β¦/{sn}/# will not match the grant and is denied. {% endhint %}
Open a standard MQTT connection using the credential fields:
Multiple POS terminals (lanes, stations) for the same merchant connect with the same username. The broker requires a unique clientId per live connection β if two clients use the same clientId, the broker disconnects the earlier one. Always append a per-terminal/per-process suffix:
The username is a safe shared identity; the clientId is what must be unique.
All topics follow the hierarchy:
Two distinct branches are used:
The device is authorized for its whole subtree MagTek/{r}/{c}/{sn}/# and listens on the concrete β¦/{sn}/SendCommand leaf β the command topic your POS credential is authorized to publish to.
The device publishes its output to the separate β¦/device/publish/{sn}/β¦ branch β never back into its own command subtree β which is why command and response do not loop. It emits two event kinds: MMSMessage (transaction/message data) and Status.
To command one device with serial TEST-001 (reseller 13204, customer 2026213):
Subscribe for its responses (do this before sending, so you don't miss the reply). A device emits two event kinds, so subscribe to both leaves:
Publish a command to that device's command topic:
Handle the response on your message callback:
You have two equivalent strategies β choose per your routing needs.
Subscribe once per event leaf to the scope wildcard and receive every device's responses on a single handler:
You do not have to build those strings yourself β they are the last two entries of mqttConfig.subscribeTopics, already correct for your scope:
This is the recommended form for anything beyond a handful of devices: two subscriptions total, and devices onboarded later are picked up automatically with no resubscribe. Extract the device serial from the topic to know who replied:
To send, publish to each device's command topic:
Subscribe to each device's two response leaves individually β these all fall under your authorized scope wildcard, so the broker permits them:
Useful when different devices feed different handlers, or when you want the subscription set to mirror exactly the devices you manage.
Your credential's topic lists enumerate the devices provisioned to your scope at the moment it was issued β a convenient starting address book. Because authorization is a scope wildcard, you are not limited to that snapshot: devices added after you connected are reachable immediately by addressing the new serial (no new credential needed).
The cleanest way to enumerate them at runtime is , which returns a roster β one entry per device, each already pairing the serial with its topics β so you never parse serials out of topic paths. Alternatives: your own provisioning records, or re-fetching the flat lists via or .
Before sending to a device you have not heard from in a while, you can ask whether it currently holds a broker session, rather than publishing and waiting on silence β use . It reports connected, status (PENDING/PROVISIONED), and blocked.
MQTT is asynchronous β a response arrives as a separate message. To match a reply to the command and device that produced it:
By device: the response topic contains the device serial, so the topic itself tells you which device replied and which event kind it is.
By command: if you issue several commands to the same device, include a correlation token in your command payload (per the device protocol) and echo it in the response, or serialize commands per device.
Keep a per-device in-flight map keyed by serial (and correlation token if available) so concurrent operations across many devices do not get cross-wired.
The JWT in password is short-lived β 8 hours by default, though configurable per environment, so always drive your timer off expiresAt rather than a hard-coded 8. You were also handed a long-lived, opaque refresh token once, at onboarding (store it securely) β it is what lets the integrated application mint fresh JWTs on its own, with no portal involvement.
Refresh ahead of expiry. Before expiresAt, mint a fresh JWT (same scope, same username) with =, passing your username + refresh token. A good practice is to refresh at ~75β80% of the lifetime. This re-mints the JWT without rotating the refresh token.
Don't let the JWT lapse β the lookup endpoints require a live one. pos-topics-list, pos-device-list, and pos-device-status
For the full credential model behind refresh, see the .
Clean vs persistent session. A clean session means you must re-subscribe on every reconnect. A persistent session (clean=false with a stable clientId) lets the broker queue messages and retain subscriptions across brief disconnects. The clientId still has to be unique across concurrently live terminals.
Multiple terminals. Same username, different clientId each. They share the identity, JWT, refresh token, and authorization scope, so each terminal is authorized for every device under the scope. Partitioning devices across lanes is a client-side concern.
Payload framing is device-defined. The MQTT platform moves opaque bytes; the SendCommand
Scope examples use reseller 13204, customer 2026213, device TEST-001.
For the complete endpoint surface β request shapes, headers, and status codes β see the .
Connect checklist
Actual response topics (one device): MagTek/13204/2026213/device/publish/TEST-001/MMSMessage and β¦/TEST-001/Status
401pos-credentials/refreshReconnect with the new token. The broker checks the JWT at CONNECT time. An existing connection is not retroactively killed at the instant of expiry, but any reconnect requires a still-valid token.
Re-subscribe after reconnect unless you use a persistent session (see below).
Security. Treat the JWT like a password β never log it. Use TLS wherever the environment offers it. Both the JWT and the refresh token are bearer secrets.
broker
Broker host to connect to. Environment-specific β always take it from this field rather than hard-coding it.
port
Broker port (1883 plain, 8883 TLS are the usual choices). Environment-specific, like broker.
peerCommonName
{
"broker": "<your-broker-host>",
"port": 8883,
"peerCommonName": "",
"username": "pos-13204-2026213",
"password": "<HS256 JWT>",
"publishTopics": ["MagTek/13204/2026213/TEST-001/SendCommand"],
"subscribeTopics": [
"MagTek/13204/2026213/device/publish/TEST-001/MMSMessage",
"MagTek/13204/2026213/device/publish/TEST-001/Status",
"MagTek/13204/2026213/device/publish/+/MMSMessage",
"MagTek/13204/2026213/device/publish/+/Status"
],
"operatingMode": "mqtts",
"expiresAt": "2026-06-19T01:43:00Z"
}client = mqtt.connect({
host: mqttConfig.broker,
port: mqttConfig.port,
username: mqttConfig.username, // CONNECT username
password: mqttConfig.password, // CONNECT password = the JWT
clientId: makeUniqueClientId(mqttConfig.username),
tls: port_is_tls, // when port is a TLS port
expectedCommonName: mqttConfig.peerCommonName // verify against server cert
})makeUniqueClientId(username) = username + "-" + stationId + "-" + randomShortId()
// e.g. "pos-13204-2026213-lane1-9fa3"MagTek / {ResellerId} / {CustomerId} / {DeviceSN}Command (to device)
MagTek/{r}/{c}/{sn}/SendCommand
POS (and backend)
the device
client.subscribe("MagTek/13204/2026213/device/publish/TEST-001/MMSMessage", qos=1)
client.subscribe("MagTek/13204/2026213/device/publish/TEST-001/Status", qos=1)topic = "MagTek/13204/2026213/TEST-001/SendCommand"
payload = buildDeviceCommand(...) // device-protocol bytes β see note below
client.publish(topic, payload, qos=1)on_message(topic, payload):
if topic.startsWith("MagTek/13204/2026213/device/publish/TEST-001/"):
handleDeviceResponse("TEST-001", topic, payload)client.subscribe("MagTek/13204/2026213/device/publish/+/MMSMessage", qos=1) // merchant scope
client.subscribe("MagTek/13204/2026213/device/publish/+/Status", qos=1)
// reseller scope would be: "MagTek/13204/+/device/publish/+/MMSMessage" (and .../+/Status)filters = mqttConfig.subscribeTopics.slice(-2) // [ .../+/MMSMessage, .../+/Status ]
for topic in filters:
client.subscribe(topic, qos=1)on_message(topic, payload):
// topic = ".../device/publish/<SN>/<MMSMessage|Status>"
parts = topic.split("/")
sn = parts[parts.length - 2]
handleDeviceResponse(sn, topic, payload)for sn in mySerials:
client.publish("MagTek/13204/2026213/" + sn + "/SendCommand", buildCommand(sn), qos=1)for sn in mySerials:
client.subscribe("MagTek/13204/2026213/device/publish/" + sn + "/MMSMessage", qos=1)
client.subscribe("MagTek/13204/2026213/device/publish/" + sn + "/Status", qos=1)loop:
if now() > expiresAt - refreshMargin:
mqttConfig = POST /api/pos-credentials/refresh { username, refreshToken }
reconnectWith(mqttConfig)Send command to one device
MagTek/13204/2026213/TEST-001/SendCommand (publish)
Receive one device's responses
MagTek/13204/2026213/device/publish/TEST-001/MMSMessage and β¦/TEST-001/Status (subscribe)
Receive all merchant devices' responses
Expected TLS certificate common name (when using TLS). May be empty.
username
MQTT username β present in the CONNECT username field. Shared by all integrated application terminals in the same scope.
password
A short-lived HS256 JWT β present in the CONNECT password field. Used for authentication (signature + expiry); the authorization rules live broker-side, not in the token.
publishTopics
List of explicit per-device command topics β one β¦/{sn}/SendCommand for every device under your scope. Informational (your real grant is a broker-side scope filter). Empty when no devices are onboarded. No wildcard entry: MQTT forbids wildcards in a PUBLISH topic.
subscribeTopics
List of per-device response topics β for each device, its β¦/device/publish/{sn}/MMSMessage and β¦/device/publish/{sn}/Status β followed, last, by the two scope filters β¦/device/publish/+/MMSMessage and β¦/+/Status. Subscribe to that trailing pair to cover every device in your scope at once.
operatingMode
Transport hint (mqtts). Treat port and your TLS settings as authoritative β see the transport note below.
expiresAt
UTC instant the JWT stops being valid. Refresh before this.
Response (from device)
MagTek/{r}/{c}/device/publish/{sn}/MMSMessage and β¦/{sn}/Status
the device
POS (and dashboards)
MagTek/13204/2026213/device/publish/+/MMSMessage and β¦/+/Status (subscribe)
Receive all reseller devices' responses
MagTek/13204/+/device/publish/+/MMSMessage and β¦/+/Status (subscribe)
Command-scope grant (merchant)
MagTek/13204/2026213/+/SendCommand (broker rule, not sendable)
