Skip to main content
Version: 1.0.8

Tutorial: Publish a REST Endpoint

Publish two endpoints an external system can call: a read endpoint returning line status, and a write endpoint accepting an order — both authenticated, scoped and rate-limited.

You will need: tags with live data, and a SQL or MongoDB connection for the write endpoint. Time: ~30 minutes.


Step 1 — Create an API key​

Keys are created where they are used. Add a REST API node to a pipeline in your project (you will configure it in Step 2), set its authentication to API Key (X-API-Key), and use Project API Keys → Create Key. It needs Access Settings.

FieldValue
Namemes-integration
Scopestag:read, db:read

Copy the key now — it is shown once, and only a hash is stored.

Note the scopes: this key can read tags and run named queries. It cannot write tags, and it cannot run arbitrary SQL. Everything below is designed around that.

→ API keys


Step 2 — Build the read endpoint​

REST API (ingress) → Python → HTTP Response

REST API node

SettingValue
Modeingress
MethodGET
URL pattern/api/v1/line-status
AuthAPI Key (X-API-Key) — a new node starts Public (No Auth), so change it
Required scopestag:read
Allowed IPsThe MES's CIDR
Rate limit / burst10 / 20
CORSoff

Five independent controls, each cheap. An integration endpoint for a known system should have all of them.

The caller names the line in the query string — ?line=Line1 — because a route is a path, not a template: there are no :line placeholders. The editor shows the address the route is served at, /gateway/<project-slug>/api/v1/line-status. The examples below use a project whose slug is plant-a.

Python node

def transform(payload, message):
line = payload["params"].get("line")
if not line:
return {"http_status": 400, "error": "line is required"}

tags = system.tag.readAll([
f"Site1/{line}/Filler/Motor1/Speed",
f"Site1/{line}/Filler/Motor1/Running",
])

if any(t["quality"] != "Good" for t in tags):
return {"http_status": 503, "error": f"line data unavailable: {line}"}

return {
"line": line,
"speed": tags[0]["value"],
"running": tags[1]["value"],
"ts": tags[0]["server_timestamp"],
}

Returning 503 on bad quality is the honest answer. Returning a stale value with a 200 tells the caller everything is fine when it is not.

HTTP Response node — every ingress path must reach one, or the caller waits for the timeout. It takes the status code from http_status (200 when absent) and sends the payload as the body; an error field answers {"error": "…"} instead.


Step 3 — Test it​

curl -H "X-API-Key: <key>" \
"https://qubiq.example.com:8090/gateway/plant-a/api/v1/line-status?line=Line1"
{"line":"Line1","speed":2450.5,"running":true,"ts":"2026-08-02T09:15:03Z"}

Then verify the controls actually work:

# no key → 401
curl "https://qubiq.example.com:8090/gateway/plant-a/api/v1/line-status?line=Line1"

# from outside the allow-list → 403
# above the rate limit → 429

Testing that the negatives fail is the part that matters.


Step 4 — Build the write endpoint​

REST API (ingress POST) → Python (validate) → Switch ─ 1 → SQL Query → HTTP Response
└ 2 → HTTP Response (the rejection)
SettingValue
MethodPOST
URL pattern/api/v1/orders
AuthAPI Key (X-API-Key)
Required scopesdb:read
Max payload size65536
Rate limit / burst5 / 10

The scope is checked at the gateway, against the route: the key must carry db:read to reach this endpoint at all. The write itself is done by the pipeline's SQL node, under the pipeline's connection — the caller never gets to say what SQL runs.

Python node — validate before anything else

def transform(payload, message):
order = payload.get("body") or {}

for field in ("orderId", "line", "qty"):
if field not in order:
return {"http_status": 400, "error": f"{field} is required"}

if not isinstance(order["qty"], int) or order["qty"] <= 0:
return {"http_status": 400, "error": "qty must be a positive integer"}

return {"order": order}

The request arrives from outside your system. Treat every field as untrusted, and reject with a message the integrator can act on.

Switch — keep a rejection away from the database

Property: payload.order

RuleOperatorOutput
1is not null→ SQL Query
2otherwise→ HTTP Response

Without it, a rejected request would carry on into the SQL node with no order to insert — the statement would fail, nothing would answer, and the caller would get a 504 instead of your 400.

The insert

Type the statement straight into the SQL node. The Python above returns {"order": …}, so the placeholders reach into that object:

INSERT INTO orders (order_id, line, qty, received_at)
VALUES ({{payload.order.orderId}}, {{payload.order.line}}, {{payload.order.qty}}, NOW())
ON CONFLICT (order_id) DO NOTHING

Each {{…}} becomes a bound parameter, so a hostile order_id cannot alter the statement — which matters more here than anywhere else, because this payload came from outside your network.

ON CONFLICT DO NOTHING makes a retried delivery safe. External systems retry.

HTTP Response after the insert — put a small Python node in front of it returning {"http_status": 201, "accepted": …} if you want the caller to see 201 rather than the SQL node's result with a 200.


Step 5 — Give the caller real status codes​

CodeWhen
200 / 201Success
400Malformed request — say which field
401Missing or invalid API key
403Valid key, missing scope, or blocked source IP
404Unknown resource
409Duplicate — already accepted
429Rate limited
503Data unavailable (bad quality, connection down)

A 200 carrying an error message is hostile to whoever integrates with you at 03:00.


Step 6 — Version and document​

Version the URL (/api/v1/...). Changing a pipeline's response shape breaks callers exactly as changing any API does — and pipelines are easy to change.

Give the integrator: the base URL and port (8090, not the UI port) with its /gateway/<project-slug> prefix, the endpoints and shapes, their API key's scopes, the rate limit, and the error codes above.


Step 7 — Operate it​

WatchWhere
Failed executionsThe execution log panel
Key usage and rejectionsAudit journal
Rate-limit hitsUsually a misconfigured client — or an attempt

The pipeline must be running for its routes to serve. A stopped pipeline returns 404, which is the first thing to check when an integrator reports the endpoint has vanished.


Security checklist​

  • Every route requires an API key — a new node starts public, so this is a change you make
  • Scopes are the narrowest that work
  • IP allow-list set for known callers
  • Rate and burst limits set slightly above real traffic
  • Max payload size bounded
  • CORS off unless a browser genuinely calls it
  • Inputs validated before any database or tag access
  • Writes are idempotent
  • Each consumer has its own key
  • Port 8090 is exposed only to the networks that need it

Next​

→ REST endpoints · API keys