A Node-RED HTTP to MQTT bridge lets any system that can send a web request trigger commands on MQTT devices, without adding an MQTT library to that system. In this tutorial you will expose a REST endpoint in Node-RED that validates the request, publishes a command to a broker, optionally waits for the device to confirm, and returns a proper HTTP status code.
When an HTTP to MQTT Bridge Makes Sense
MQTT is the right protocol between devices and the broker, but plenty of software around a plant or a building only knows HTTP:
- An ERP or MES that can call a URL when a work order starts.
- A webhook from a SaaS product, a payment provider or a form tool.
- Shell scripts and cron jobs that already use
curl. - Mobile apps or dashboards that you do not want holding long-lived MQTT connections.
- A third-party integrator who will only accept a REST contract.
Instead of teaching each of these systems MQTT, you give them a small, well-defined HTTP API. Node-RED translates every request into a publish on the right topic. The devices remain pure MQTT clients, and the broker remains the single place where messages flow.
Architecture of the Flow
HTTP client --POST /api/devices/pump1/command--> Node-RED
Node-RED --publish connectmq/demo/h7r2pq/pump1/cmd--> MQTT broker --> device
device --publish connectmq/demo/h7r2pq/pump1/ack--> MQTT broker --> Node-RED
Node-RED --HTTP 200 or 202 or 504--> HTTP client
We will build three endpoints:
| Method and path | Purpose | Response |
|---|---|---|
POST /api/devices/:id/command |
Publish a command and return immediately | 202 Accepted |
POST /api/devices/:id/command?wait=1 |
Publish and wait for the device acknowledgement | 200 OK or 504 Gateway Timeout |
GET /api/devices/:id/state |
Return the latest known state from MQTT | 200 OK or 404 Not Found |
For the demo we use the free public MQTT broker at broker.connectmq.com with the prefix connectmq/demo/h7r2pq. Use your own random prefix, and never send real control commands through the public area, since anyone can read and publish there.
Step 1: The HTTP Endpoint and Validation
Add an http in node with method POST and URL /api/devices/:id/command. Node-RED parses JSON bodies automatically when the client sends Content-Type: application/json, and puts path parameters into msg.req.params.
Connect it to a function node with two outputs: the first goes to MQTT, the second returns errors directly to an http response node.
const PREFIX = "connectmq/demo/h7r2pq";
const ALLOWED_DEVICES = ["pump1", "pump2", "valve3"];
const ALLOWED_ACTIONS = ["start", "stop", "setpoint"];
const API_KEY = env.get("BRIDGE_API_KEY"); // set in the flow or environment
function fail(code, message) {
msg.statusCode = code;
msg.payload = { error: message };
return [null, msg];
}
if (!API_KEY || msg.req.headers["x-api-key"] !== API_KEY) {
return fail(401, "invalid or missing API key");
}
const id = msg.req.params.id;
if (!ALLOWED_DEVICES.includes(id)) return fail(404, "unknown device " + id);
const body = msg.payload;
if (typeof body !== "object" || body === null) return fail(400, "JSON body required");
if (!ALLOWED_ACTIONS.includes(body.action)) return fail(400, "action must be one of " + ALLOWED_ACTIONS.join(", "));
if (body.action === "setpoint" && typeof body.value !== "number") {
return fail(400, "setpoint requires a numeric value");
}
const correlationId = Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
const command = {
action: body.action,
value: body.value,
correlationId,
issuedAt: new Date().toISOString(),
source: "http"
};
msg.deviceId = id;
msg.correlationId = correlationId;
msg.wait = msg.req.query.wait === "1";
msg.topic = `${PREFIX}/${id}/cmd`;
msg.payload = JSON.stringify(command);
msg.qos = 1;
msg.retain = false;
return [msg, null];
Never build the MQTT topic directly from user input without checking it against an allow list. A request for device # or ../../other would otherwise publish to topics you never intended to expose.
Step 2: Publish and Respond
Wire the first output to two places:
- An mqtt out node with an empty topic field, so it uses
msg.topic, connected to your broker config node. - A function node that decides how to answer the HTTP caller:
if (msg.wait) {
// Park the request until the device acknowledges or the timer fires
const pending = flow.get("pending") || {};
pending[msg.correlationId] = msg;
flow.set("pending", pending);
setTimeout(() => {
const p = flow.get("pending") || {};
const waiting = p[msg.correlationId];
if (waiting) {
delete p[msg.correlationId];
flow.set("pending", p);
waiting.statusCode = 504;
waiting.payload = { error: "device did not acknowledge in time", correlationId: msg.correlationId };
node.send(waiting);
}
}, 5000);
return null;
}
msg.statusCode = 202;
msg.payload = { status: "accepted", correlationId: msg.correlationId, topic: msg.topic };
return msg;
Wire this function and the error output from Step 1 into the same http response node. The response node uses msg.statusCode and sends msg.payload as JSON.
The asynchronous 202 answer is the right default. MQTT devices can be asleep, reconnecting or busy, and an HTTP client should not hang on that. The wait=1 variant is useful for interactive tools where a person clicks a button and expects to know whether the pump actually started.
Step 3: Handle Device Acknowledgements
The device publishes an acknowledgement that echoes the correlation ID, for example on connectmq/demo/h7r2pq/pump1/ack:
{ "correlationId": "lrx2k9ab12cd", "result": "ok", "state": "running" }
Subscribe with an mqtt in node to connectmq/demo/h7r2pq/+/ack (output: parsed JSON) and match it to the parked request:
const ack = msg.payload;
const pending = flow.get("pending") || {};
const waiting = pending[ack.correlationId];
if (!waiting) return null; // already timed out, or a 202 request
delete pending[ack.correlationId];
flow.set("pending", pending);
waiting.statusCode = ack.result === "ok" ? 200 : 409;
waiting.payload = { status: ack.result, state: ack.state, correlationId: ack.correlationId };
return waiting;
Wire it to the same http response node. Because the parked message still carries the original msg.res object, the response goes back to the right HTTP caller. Pending requests live in memory, which is fine because the HTTP connections they belong to also disappear when Node-RED restarts.
If you use MQTT 5, you can set msg.responseTopic and msg.correlationData on the outgoing command instead of embedding the ID in the JSON. The mqtt out node passes them as MQTT 5 properties, and devices that support them can reply on the requested topic. The MQTT 5 features guide explains request and response in more detail.
Step 4: A Read Endpoint for Device State
Devices should publish their state as retained messages to connectmq/demo/h7r2pq/<id>/state. Subscribe to connectmq/demo/h7r2pq/+/state and store each one in flow context:
const id = msg.topic.split("/")[3];
const states = flow.get("states") || {};
states[id] = { state: msg.payload, receivedAt: new Date().toISOString() };
flow.set("states", states);
return null;
Then add an http in node for GET /api/devices/:id/state:
const states = flow.get("states") || {};
const s = states[msg.req.params.id];
if (!s) {
msg.statusCode = 404;
msg.payload = { error: "no state received for this device" };
} else {
msg.statusCode = 200;
msg.payload = s;
}
return msg;
Because the state messages are retained, Node-RED receives the latest value for every device as soon as it reconnects, so the read endpoint is useful immediately after a restart.
Testing with curl
Set the API key as an environment variable for Node-RED (BRIDGE_API_KEY=change-me), deploy, and call the endpoints on the default port 1880:
# Fire and forget
curl -i -X POST http://localhost:1880/api/devices/pump1/command \
-H "Content-Type: application/json" -H "X-API-Key: change-me" \
-d '{"action":"start"}'
# Wait for acknowledgement (returns 504 after 5 s if no device answers)
curl -i -X POST "http://localhost:1880/api/devices/pump1/command?wait=1" \
-H "Content-Type: application/json" -H "X-API-Key: change-me" \
-d '{"action":"setpoint","value":42.5}'
# Simulate the device acknowledging (copy the correlationId from the cmd topic)
mosquitto_sub -h broker.connectmq.com -p 1883 -i dev-h7r2pq -v -t "connectmq/demo/h7r2pq/pump1/cmd"
mosquitto_pub -h broker.connectmq.com -p 1883 -i ack-h7r2pq -t "connectmq/demo/h7r2pq/pump1/ack" \
-m '{"correlationId":"PASTE_ID","result":"ok","state":"running"}'
Securing a Node-RED HTTP to MQTT Bridge
An HTTP endpoint that moves pumps is an attack surface. Work through this list before exposing it beyond localhost:
- Authenticate every request. The API key check above is a minimum. For basic authentication across all HTTP-in endpoints, set
httpNodeAuthinsettings.jswith a bcrypt hash generated bynode-red admin hash-pw. - Terminate TLS in front of Node-RED with a reverse proxy such as Nginx or Caddy. Do not expose port 1880 directly to the internet.
- Protect the editor with
adminAuth, or run the editor on a different, internal-only path or port. - Validate inputs against allow lists for devices and actions, and type-check values.
- Rate-limit at the proxy, and remember that the broker enforces limits too. The public broker allows 10 messages per second per client, and production plans have their own quotas.
- Use private broker credentials over TLS on port 8883 for the mqtt nodes.
Our article on securing Node-RED covers the editor and settings side in depth.
Moving to a Production Broker
Replace the demo broker config with a private ConnectMQ broker: port 8883, TLS enabled, and the YOUR_USERNAME, YOUR_PASSWORD and YOUR_CLIENT_ID values generated in the portal. ConnectMQ does not offer webhooks or an HTTP publish API of its own, and that is exactly why this pattern exists: Node-RED is the HTTP-facing layer, and the broker stays focused on MQTT. Running the broker as a hosted MQTT broker means the only server you look after is the Node-RED instance.
Run Your Node-RED Bridge on ConnectMQ
Create a broker for free at app.connectmq.com on the Serverless plan, no credit card required, and point your Node-RED bridge at it over TLS. You can watch commands and acknowledgements in the portal's live topic explorer while you test your endpoints with curl. For the fundamentals of the MQTT nodes, see the Node-RED MQTT tutorial, and for the reverse direction, sending alerts to people, see the Node-RED Telegram bot guide.
Frequently Asked Questions
Can Node-RED receive webhooks and publish them to MQTT?
Yes. An http in node accepts the webhook, a function node validates it and builds a topic, and an mqtt out node publishes it. Return a quick 2xx response so the webhook sender does not retry.
Should the HTTP endpoint wait for the device to respond?
Return 202 Accepted by default and let clients check state separately. Waiting for an acknowledgement is useful for interactive use, but always enforce a short timeout.
Which QoS should I use for commands from HTTP?
QoS 1 is a good default for commands, combined with idempotent actions such as start and stop rather than toggle, so a duplicate delivery does no harm.
Does ConnectMQ provide a REST publish API?
No. ConnectMQ is an MQTT broker with MQTT, TLS and WebSocket endpoints. Use Node-RED or a small service of your own to translate HTTP requests into MQTT publishes.



