Remote control
Open and close barriers, gates, chains, bollards and brains from your backend, and know for sure whether each command worked. This guide turns the Quickstart into something you can run in production.
Prerequisites
- An API token on a server; see Authentication.
- A subscription with remote control for each product; see Plans and permissions.
- Each product paired, on a lot, with an online Gateway in Bluetooth range on the same lot. See What you need.
- The Socket.IO client for your language; see Connecting.
How a command works
You send a command, the API answers at once with an event_id, and the result arrives
later as a realtime event: lot_change on success, gateway_error on failure. The API
tries each Gateway that sees the product, strongest signal first, for up to 30 seconds
each. The details are in Asynchronous operations.
1. Check the operation fits the product
The API doesn't check that an operation makes sense for the product type; check it
yourself before sending. product_type_id comes from
GET /v2/products/{id}.
const BARRIER = 2;
const BRAIN_TYPES = [3, 4, 5, 6]; // gate, brain, chain, bollard
const ALLOWED: Record<string, number[]> = {
open: [BARRIER, ...BRAIN_TYPES],
close: [BARRIER, ...BRAIN_TYPES],
force_close: [BARRIER],
stay_open: BRAIN_TYPES,
stay_close: BRAIN_TYPES,
clear_stay_open: BRAIN_TYPES,
clear_stay_close: BRAIN_TYPES,
};
export function canRun(product: { status: string; product_type_id: number }, operation: string) {
return product.status === 'online' && (ALLOWED[operation] ?? []).includes(product.product_type_id);
}
The matrix, and what open and close mean on a barrier (open lowers the arm), is in
Products and operations.
2. Connect, send, and wait
Connect to realtime before you send anything, and keep the connection open for the
life of your service. Then, for each command, send it and wait for the event that
carries the same event_id:
- Node.js
- Python
const API = 'https://api.parklio.com/v2';
const headers = {
Authorization: `Bearer ${process.env.PARKLIO_API_TOKEN}`,
'Content-Type': 'application/json',
};
// Connect once, before sending anything.
const socket = io(`${API}/realtime`, {
path: '/socketio',
query: { token: process.env.PARKLIO_API_TOKEN },
});
export async function runOperation(productId: number, operation: string) {
// Listen before sending: the result can arrive before the HTTP response does.
const results = new Map<string, { ok: boolean; event: any }>();
let arrived = () => {};
const record = (ok: boolean) => (raw: string) => {
const event = JSON.parse(raw);
if (!event.event_id) return; // a status update, not a command result
results.set(event.event_id, { ok, event });
arrived();
};
const onSuccess = record(true);
const onFailure = record(false);
socket.on('lot_change', onSuccess);
socket.on('gateway_error', onFailure);
let timedOut = false;
const timer = setTimeout(() => {
timedOut = true;
arrived();
}, 120_000);
try {
const res = await fetch(`${API}/products/${productId}/state`, {
method: 'POST',
headers,
body: JSON.stringify({ operation }),
});
const body = await res.json();
if (!body.success) {
// Rejected before anything was sent to the device.
return { ok: false, rejected: true, code: body.errors[0].code, message: body.errors[0].message };
}
const eventId: string = body.data.event_id;
while (!results.has(eventId) && !timedOut) {
await new Promise<void>((resolve) => (arrived = resolve));
}
const result = results.get(eventId);
if (!result) return { ok: false, unknown: true };
const { event } = result;
if (result.ok) {
const { data } = event;
const block = data.barrier ?? data.gate ?? data.chain ?? data.bollard ?? data.brain;
return { ok: true, state: block?.state };
}
return { ok: false, type: event.type, message: event.message };
} finally {
clearTimeout(timer);
socket.off('lot_change', onSuccess);
socket.off('gateway_error', onFailure);
}
}
import json
import os
import threading
import httpx
import socketio
API = "https://api.parklio.com/v2"
TOKEN = os.environ["PARKLIO_API_TOKEN"]
HEADERS = {"Authorization": f"Bearer {TOKEN}"}
NS = "/v2/realtime"
sio = socketio.Client()
results = {} # event_id -> (ok, event)
arrived = threading.Condition()
def record(ok):
def handler(raw):
event = json.loads(raw)
if not event.get("event_id"):
return # a status update, not a command result
with arrived:
results[event["event_id"]] = (ok, event)
arrived.notify_all()
return handler
sio.on("lot_change", record(True), namespace=NS)
sio.on("gateway_error", record(False), namespace=NS)
# Connect once, before sending anything.
sio.connect(f"https://api.parklio.com?token={TOKEN}", namespaces=[NS], socketio_path="socketio")
def run_operation(product_id, operation):
body = httpx.post(
f"{API}/products/{product_id}/state", headers=HEADERS, json={"operation": operation}
).json()
if not body["success"]:
# Rejected before anything was sent to the device.
error = body["errors"][0]
return {"ok": False, "rejected": True, "code": error["code"], "message": error["message"]}
event_id = body["data"]["event_id"]
with arrived:
if not arrived.wait_for(lambda: event_id in results, timeout=120):
return {"ok": False, "unknown": True}
ok, event = results.pop(event_id)
if ok:
data = event["data"]
block = next(
(data[k] for k in ("barrier", "gate", "chain", "bollard", "brain") if data.get(k)),
None,
)
return {"ok": True, "state": block and block["state"]}
return {"ok": False, "type": event["type"], "message": event.get("message")}
The function has four outcomes: rejected (an HTTP error, nothing was sent), ok,
failed (a gateway_error), and unknown (you gave up waiting). Handle each one.
3. Handle failures
Rejected: an HTTP error
Nothing reached the device. Act on the error code:
| Code | Meaning | What to do |
|---|---|---|
214 | Another operation on this product is still running. | Wait for its result, then send yours. |
704 | No Gateway sees the product right now. | Don't retry at once; check the product's status and the Gateway. |
30 | The product's subscription doesn't include remote control. | Fix the subscription; retrying won't help. |
206 | The product isn't paired. | Pair it with the Connect app. |
6 / 4 | The product doesn't exist, or your token can't see it. | Check the id and the product's lot. |
23 | Invalid body. | Fix the request. |
214 can also come from operations you didn't send: someone using the Connect app, or
Parklio setting the device's clock. It clears as soon as that operation's result is
sent, which can take up to 30 seconds per Gateway.
Failed: a gateway_error
The command reached at least one Gateway and didn't succeed. type tells you why; the
full table is in Events.
type | What to do |
|---|---|
6 Car on top | The barrier won't rise with a car over it. Tell the user; don't retry until the car has left. |
5 Device jammed, 7 Sensor dirty | Needs someone on site. Alert, don't retry. |
0 Auth error, 1 Invalid guest key, 8 Device not initialized | The device rejected the command. Retrying won't help; contact support if it persists. |
2, 3, 4, 10, 11, "Timeout" | The API already tried every Gateway that sees the product. One later retry is reasonable; alert if it fails again. |
9 Rtcc not set | The device's clock isn't set. Parklio sets it automatically when a Gateway sees the device (at most one attempt an hour per device); retry later. |
Unknown: you gave up waiting
The command may still have run. Re-read the product with
GET /v2/products/{id} and compare its state with
what you asked for before doing anything else. Don't resend blindly: on a barrier, a
second close after a successful one is harmless, but your users may see the arm move
twice.
4. Send one command per product at a time
Queue commands per product in your service, and send the next only when the previous
one has a result (or you gave up and re-read the state). This avoids 214 and keeps
results easy to match. Commands for different products can run in parallel.
Change barrier settings
POST /v2/products/{id}/settings works the
same way: send one setting per call, get an event_id, wait for the result.
curl -s -X POST https://api.parklio.com/v2/products/123/settings \
-H "Authorization: Bearer $PARKLIO_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"autoclose_time": 30}'
autoclose_time is 5–60 seconds. To turn auto-close on or off, send
{"is_autoclose": true} or false (a JSON boolean). In the lot_change result the
barrier block calls these auto_close_time and is_auto_close; see
Events.
Brains without input sensors
A brain wired to a gate or door with no open/closed sensor can't report whether it
moved. Send "wait_for_completion": false with the operation, so the Gateway doesn't
wait for a state change that will never come:
{ "operation": "open", "wait_for_completion": false }
Verify it worked
runOperationreturnsok: true, and the state matches the operation:openedafteropen,closedafterclose.GET /v2/products/{id}shows the same state in its type block.- On site, the product moved. On a barrier,
openlowers the arm.
Common errors
| Symptom | Cause | Fix |
|---|---|---|
Always 704 | No Gateway sees the product. | Check the Gateway is online, on the same lot and in range; see Troubleshooting. |
Frequent 214 | Commands for one product overlap. | Queue per product (step 4). |
No result, then "Timeout" | The Gateway couldn't reach the product over Bluetooth. | Check range and power; a result can take 30 seconds per Gateway. |
| Results never arrive | You connected after sending, or the connection dropped. | Connect first; resync after reconnects (Patterns). |
An unexpected error (such as HTTP 500, code 0) for stay_open on a barrier | Unsupported operation for the product type. | Check the matrix (step 1). |
403 code 30 | No remote control in the subscription. | See Plans and permissions. |
Next
- Monitor devices: know when a product needs attention.
- Events