Skip to main content

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}.

operations.ts
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:

run-operation.ts
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);
}
}

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:

CodeMeaningWhat to do
214Another operation on this product is still running.Wait for its result, then send yours.
704No Gateway sees the product right now.Don't retry at once; check the product's status and the Gateway.
30The product's subscription doesn't include remote control.Fix the subscription; retrying won't help.
206The product isn't paired.Pair it with the Connect app.
6 / 4The product doesn't exist, or your token can't see it.Check the id and the product's lot.
23Invalid 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.

typeWhat to do
6 Car on topThe 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 dirtyNeeds someone on site. Alert, don't retry.
0 Auth error, 1 Invalid guest key, 8 Device not initializedThe 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 setThe 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.

Set the auto-close delay to 30 seconds
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:

Request body
{ "operation": "open", "wait_for_completion": false }

Verify it worked​

  1. runOperation returns ok: true, and the state matches the operation: opened after open, closed after close.
  2. GET /v2/products/{id} shows the same state in its type block.
  3. On site, the product moved. On a barrier, open lowers the arm.

Common errors​

SymptomCauseFix
Always 704No Gateway sees the product.Check the Gateway is online, on the same lot and in range; see Troubleshooting.
Frequent 214Commands 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 arriveYou 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 barrierUnsupported operation for the product type.Check the matrix (step 1).
403 code 30No remote control in the subscription.See Plans and permissions.

Next​