Skip to main content
This is the contract every MQTT-capable device — ESP32 or otherwise — must implement to connect to PumpLink.

Connection

  • Broker: mqtt.pumplink.dev
  • Port: 42743
  • Transport: TLS required. Plain TCP is not accepted for external connections.
  • Credentials:
    • Username: the device’s numeric database ID, as a string.
    • Password: the 24-byte plaintext token shown once in the admin portal when you register a device or regenerate its credentials. It’s bcrypt-hashed server-side and never recoverable after that — your provisioning flow must capture it and burn it into firmware or device config at manufacture or setup time.
  • Client ID: unconstrained. Not checked by the backend.
Credentials are issued once and never recoverable. If you lose the plaintext token, you must regenerate credentials from the admin portal and reflash the device. Store it securely at provisioning time.
You can test your device connection using any MQTT client such as MQTT Explorer. Connect to mqtt.pumplink.dev on port 42743 with TLS enabled, using your device’s username and password. Subscribe to device/{id}/# to observe all traffic for your device.

Topics

All topics are scoped to device/{deviceID}/. The ACL denies anything outside this prefix. There are no JSON envelopes anywhere in this contract — all three topics carry bare strings.

Activation handshake

This is the core contract every device must implement:
1

Subscribe

Device subscribes to device/{id}/command.
2

Backend activates

On POST /activate, the backend publishes "on" (QoS 2, not retained).
3

Device must ack on

The device must publish anything to device/{id}/ack within 10 seconds. If it doesn’t, the backend aborts, publishes "off", and returns a 504 to the caller.
4

Device runs until told to stop

The device runs until it receives "off" on the command topic. There’s no session-duration or quota message sent to the device — the backend’s own timer decides when to cut it off. Timer expiry, admin force-shutdown, and a graceful backend shutdown all just result in an unsolicited "off".
5

Device must ack off

The device must publish anything to device/{id}/ack within 10 seconds of receiving "off", the same as it does for "on". This applies to every "off", not just ones the device requested — timer expiry, admin force-shutdown, and ack timeout aborts all require the same ack back.
A device that doesn’t ack "off" in time is recorded as UNKNOWN, not OFF — the backend won’t assume the device actually turned off just because it stopped hearing from it. A device in UNKNOWN state is refused re-activation, and the rejection happens before any command is published, so the device can’t ack its way out: the only way it clears itself is by self-reporting its real state on device/{id}/status. (An admin can also physically check the pump and clear it with POST /device/:id/confirm-off, but that’s a manual escape hatch, not the normal path.)Publish your current state to device/{id}/status on every connect. This is the single most important thing your firmware can do to stay out of trouble — without it, a device that missed an "off" while briefly offline stays stuck in UNKNOWN and needs an admin to intervene.

Client behavior expectations

  • Retained messages: none, on any topic.
  • Keepalive / clean session: no specific requirement is enforced by the backend.
Your device does not need to register its own MQTT LWT (Last Will and Testament). The broker itself detects session loss — clean disconnect, keepalive timeout, or dropped TCP — and reports it to the backend regardless of device-side configuration, so ungraceful power loss is caught even if your firmware never gets a chance to publish anything on the way down.A confirmed disconnect while a session is active force-ends it the same way an ack timeout does — the backend publishes "off" to a device that can no longer hear it, gets no ack, and marks the device UNKNOWN until it later confirms its real state. This is reported with a distinct reason from a device’s own self-reported "off", so the two aren’t conflated.A device that drops off MQTT while idle is treated differently: its state is left untouched. Losing a broker session says nothing about whether the pump is physically running, so a device that was confirmed OFF stays OFF and can be activated as soon as it’s back. Connectivity is tracked as its own signal rather than folded into device state.Publishing "off" yourself on graceful shutdown is still fine to do — it’s just no longer required for the backend to notice you’re gone.

Quota

Each device has its own rolling 24-hour quota, enforced entirely server-side. The device is quota-blind and just responds to on/off commands. Quota is checked before anything is published. The full requested duration is reserved at activation time, and a request that doesn’t fit is rejected with a 429 — the device is never commanded at all. A running session is never cut short because quota ran out mid-run, so your firmware never has to handle that case. Usage is credited once the "off" outcome is known — acked or timed out — not at the moment the backend decides to end the session, so a slow ack doesn’t undercount actual on-time. A session that never started credits zero.