Skip to main content
Version: 1.0.4

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

Settings → API keys → New.

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/lines/:line/status
Auth typeapi_key
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.

Python node

def transform(payload, message):
line = payload["params"]["line"]

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 {"status": 503,
"body": {"error": "line data unavailable", "line": line}}

return {"status": 200, "body": {
"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.


Step 3 — Test it

curl -H "X-API-Key: <key>" \
https://qubiq.example.com:8090/api/v1/lines/Line1/status
{"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/api/v1/lines/Line1/status

# 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) → SQL Query → HTTP Response
SettingValue
MethodPOST
URL pattern/api/v1/orders
Auth typeapi_key
Required scopesdb:read
Max payload size65536
Rate limit / burst5 / 10

Note the endpoint requires only db:read, because the write goes through a named query — which is exactly why named queries let integrations run at lower privilege.

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 {"status": 400,
"body": {"error": f"{field} is required"}}

if not isinstance(order["qty"], int) or order["qty"] <= 0:
return {"status": 400,
"body": {"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.

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.


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), 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 — public is a deliberate choice, made once
  • 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