Skip to main content

Examples

Complete scripts for the common scenarios, in Node.js (TypeScript) and Python. Each one is type-checked or compiled on every docs build, and the code on the guide pages comes from the same files.

Setup​

  • An API token; see Authentication. The scripts read it from PARKLIO_API_TOKEN, and ids from PRODUCT_ID and LOT_ID.
  • Node.js 24+, which runs TypeScript files directly, and socket.io-client 4.
  • Python 3.10+, with httpx and python-socketio 5.
Install and configure
npm install socket.io-client # Node.js
pip install httpx "python-socketio[client]" # Python

export PARKLIO_API_TOKEN="<your-api-token>"
export PRODUCT_ID=123
export LOT_ID=7

Run a script with node open.ts or python3 open.py.

Open a product and wait for the result​

Connect to realtime, send open, and wait for the lot_change or gateway_error with your event_id. Explained in the Quickstart.

open.ts
import { io } from 'socket.io-client';

const token = process.env.PARKLIO_API_TOKEN;
const productId = process.env.PRODUCT_ID;

interface OperationEvent {
event_id?: string;
type?: number | 'Timeout';
message?: string;
data: Record<string, any>;
}
type Result = { ok: boolean; event: OperationEvent };

// 1. Connect first. `connect` fires once the server has accepted the token.
const socket = io('https://api.parklio.com/v2/realtime', {
path: '/socketio',
query: { token },
});
await new Promise((resolve, reject) => {
socket.once('connect', () => resolve(undefined));
socket.once('connect_error', reject);
});

// 2. Collect results by event_id. One can arrive before the HTTP response does.
const results = new Map<string, Result>();
const waiters = new Map<string, (result: Result) => void>();
const settle = (ok: boolean) => (raw: string) => {
const event: OperationEvent = JSON.parse(raw); // payloads are JSON strings
if (!event.event_id) return; // a status update nobody requested
const waiter = waiters.get(event.event_id);
if (waiter) waiter({ ok, event });
else results.set(event.event_id, { ok, event });
};
socket.on('lot_change', settle(true));
socket.on('gateway_error', settle(false));

// 3. Send the command.
const res = await fetch(`https://api.parklio.com/v2/products/${productId}/state`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ operation: 'open' }),
});
const body = await res.json();
if (!body.success) throw new Error(JSON.stringify(body.errors));
const eventId: string = body.data.event_id;
console.log('Sent, event_id', eventId);

// 4. Wait for the matching event.
const { ok, event } = await new Promise<Result>((resolve) => {
const early = results.get(eventId);
if (early) resolve(early);
else waiters.set(eventId, resolve);
});
console.log(ok ? `Done: ${JSON.stringify(event.data.barrier ?? event.data)}` : `Failed: ${event.message ?? event.type}`);
socket.close();

Monitor devices​

Alert on offline products, error flags and low batteries from status updates, and watch Gateways. Explained in Monitor devices.

monitor-devices.ts
// The monitor-devices guide: status updates, offline products, Gateways.
import { io } from 'socket.io-client';

const socket = io('https://api.parklio.com/v2/realtime', {
path: '/socketio',
query: { token: process.env.PARKLIO_API_TOKEN },
});

// alert() and resolve() stand for your alerting.
const last = new Map<number, { online: boolean; errorCode: string | null; low: boolean }>();

socket.on('lot_change', (raw: string) => {
const { event_id, data } = JSON.parse(raw);
if (event_id) return; // command results are handled elsewhere

const online = data.status === 'online';
const errorCode: string | null = data.product_error?.error_code ?? null;
const low = isLowBattery(data.battery_status);
const prev = last.get(data.id);
last.set(data.id, { online, errorCode, low });

if (!online && prev?.online) alert(data, 'offline');
if (online && prev?.online === false) resolve(data, 'back online');
if (errorCode && errorCode !== prev?.errorCode) alert(data, `error flags ${errorCode}`, data.product_error);
if (!errorCode && prev?.errorCode) resolve(data, 'error cleared');
if (low && !prev?.low) alert(data, `battery at ${data.battery_status}%`);
});

function isLowBattery(value: unknown) {
// Only barriers report a battery level. 0–100 is percent; above 100 means grid power.
return typeof value === 'number' && value <= 20;
}

socket.on('gateway_status', (raw: string) => {
const { uuid, status } = JSON.parse(raw);
if (status === 'offline') alert({ uuid }, 'Gateway offline');
});

// Your alerting.
function alert(subject: Record<string, any>, what: string, details?: unknown) {
console.warn('ALERT', subject.id ?? subject.uuid, what, details ?? '');
}
function resolve(subject: Record<string, any>, what: string) {
console.info('RESOLVED', subject.id ?? subject.uuid, what);
}

Create an operation token for your app​

Mint a short-lived token for one product, to hand to your app. Explained in Operation tokens for your app.

operation-token.ts
// The operation-tokens guide: mint a short-lived token for your app.

export async function createOperationToken(productId: number, minutes: number) {
const start = new Date();
const end = new Date(start.getTime() + minutes * 60_000);
const res = await fetch('https://api.parklio.com/v2/operation-tokens', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.PARKLIO_API_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
product_id: productId,
start_time: start.toISOString(),
end_time: end.toISOString(),
}),
});
const body = await res.json();
if (!body.success) throw new Error(`${body.errors[0].code}: ${body.errors[0].message}`);
return body.data;
}

if (import.meta.main) {
const token = await createOperationToken(Number(process.env.PRODUCT_ID), 15);
// Hand these to your app, unchanged.
const { token_open, token_close, token_status, end_time, product } = token;
console.log({ device_id: product.device_id, end_time, token_open, token_close, token_status });
}

Whitelist plates and bind them to a product​

Add plates to a lot's whitelist, bind them to a product, and check the binding. Explained in Plate-based access.

plate-access.ts
// The plate-based-access guide: whitelist plates on a lot, bind them to a product.
const API = 'https://api.parklio.com/v2';
const headers = {
Authorization: `Bearer ${process.env.PARKLIO_API_TOKEN}`,
'Content-Type': 'application/json',
};

async function call(method: string, path: string, body?: unknown) {
const init = body === undefined ? { method, headers } : { method, headers, body: JSON.stringify(body) };
const res = await fetch(`${API}${path}`, init);
const json = await res.json();
if (!json.success) throw new Error(`${path}: ${JSON.stringify(json.errors)}`);
return json;
}

export async function whitelistAndBind(lotId: number, productId: number, plates: string[], until: Date) {
// 1. Whitelist the plates on the lot. One result per plate.
const { data: results } = await call('POST', '/lot-whitelists', {
lot_id: lotId,
name: 'Tenants',
license_plates: plates,
start_time: new Date().toISOString(),
end_time: until.toISOString(),
});
const failed = results.filter((r: { success: boolean }) => !r.success);
if (failed.length) console.warn('Not whitelisted:', failed);
const licenseIds = results.filter((r: { success: boolean }) => r.success).map((r: { license_id: number }) => r.license_id);

// 2. Bind them to the product. Unknown ids are ignored silently...
await call('POST', `/products/${productId}/bind-licenses`, { license_ids: licenseIds });

// 3. ...so check what's bound.
const { data: bound } = await call('GET', `/products/${productId}/bound-licenses?size=300`);
const boundIds = new Set(bound.map((b: { id: number }) => b.id));
return licenseIds.filter((id: number) => !boundIds.has(id)); // ids that didn't bind
}

if (import.meta.main) {
const until = new Date(Date.now() + 30 * 24 * 3600_000);
const missing = await whitelistAndBind(Number(process.env.LOT_ID), Number(process.env.PRODUCT_ID), ['ZG1234AB'], until);
console.log(missing.length ? `Not bound: ${missing}` : 'Whitelisted and bound');
}

Live occupancy​

Keep free-space counts per zone up to date from zone_change events, and re-read the zones every minute for changes that send no event. Explained in Live occupancy.

live-occupancy.ts
// The live-occupancy guide: read zone counts, keep them live, re-read regularly.
import { io } from 'socket.io-client';

const LOT_ID = Number(process.env.LOT_ID);
const headers = { Authorization: `Bearer ${process.env.PARKLIO_API_TOKEN}` };
const socket = io('https://api.parklio.com/v2/realtime', {
path: '/socketio',
query: { token: process.env.PARKLIO_API_TOKEN },
});

const zones = new Map<number, { spot_count: number; occupied_count: number }>();

socket.on('zone_change', (raw: string) => {
for (const zone of JSON.parse(raw).zones) {
zones.set(zone.id, { spot_count: zone.spot_count, occupied_count: zone.occupied_count });
render(zone.id); // your UI
}
});

function render(zoneId: number) {
const zone = zones.get(zoneId);
if (zone) console.log(`zone ${zoneId}: ${zone.spot_count - zone.occupied_count} free`);
}

// LPR, payment-machine and recount changes send no event: re-read every minute.
async function readZones(lotId: number) {
const url = `https://api.parklio.com/v2/zones?filter=lot_id:eq:${lotId}&size=300`;
const body = await (await fetch(url, { headers })).json();
for (const zone of body.data) {
zones.set(zone.id, { spot_count: zone.spot_count, occupied_count: zone.occupied_count });
render(zone.id);
}
}
socket.on('connect', () => readZones(LOT_ID).catch(console.error));
setInterval(() => readZones(LOT_ID).catch(console.error), 60_000);

Next​