Contribute parking data (You -> Parksby)
Add one car park from the app, bulk-import a portfolio with your own column names, or send live occupancy from a system you already run. None of it needs a payout account or a booking arrangement.
Overview
Put your car park in front of drivers looking for parking — and, if it knows how full it is, show them that too. You don't need to take bookings through Parksby to be on the map.
| You have | Use |
|---|---|
| One car park, or a handful | The mobile app — add it in a minute |
| Many car parks in a spreadsheet | The bulk import, with your own column names |
| A system that knows how full you are | The occupancy API |
| A city, national or network feed (your own ids, FIWARE or DATEX II) | Publisher API |
| Cameras, barriers or counters at the gate | A gate connection in Spaces (see Hardware ingest below) |
Nothing here needs a payout account or a booking arrangement.
Add one parking
Open Parksby, tap Add parking, drop the pin. You are asked for a name and a location; everything else is optional and can be filled in later or by other drivers.
You do not have to own the place. If you do own it, you can additionally choose to take bookings — but only in countries where Parksby can settle money, and the app will tell you which.
Bulk import
For a portfolio of car parks. Send a CSV; we match your column headers rather than demanding ours.
Required: name, latitude, longitude. That is all.
Everything else is optional, and common alternative spellings are recognised automatically:
| Field | Headers we match |
|---|---|
name |
name, title, facility, facility_name, bezeichnung |
latitude |
latitude, lat, y |
longitude |
longitude, lon, lng, long, x |
capacity_estimate |
capacity_estimate, capacity, total, spaces, total_spaces |
site_type |
site_type, type, kind, lot_type — garage / surface_lot / onstreet_zone |
address |
address, street, adresse |
operator_label |
operator_label, operator, owner |
currency |
currency, ccy — ISO 4217, e.g. KES, EUR |
hourly_rate, daily_rate, flat_rate |
hourly, daily, flat, price, price_hour, price_day |
hours_text |
hours_text, hours, opening_hours, oeffnungszeiten |
payment_methods, security_features |
pipe-separated: cash|mpesa |
restrictions, notes, country_code |
as named |
A row with rates needs a currency. An amount without one is not a price, and we will not guess.
Before anything is written you see exactly what would happen: which rows would be created, which were rejected and why, and which sit near a facility already on the map with a similar name. Your column mapping is remembered, so a second upload is one click.
Imported car parks are checked by our team before they appear on the map.
Talk to us to get set up.
Occupancy API
If your barrier system, guidance panel or counting hardware knows how full you are, send it. No booking relationship required.
Getting a key. Once a car park is yours on Parksby (claimed or listed),
open Spaces → API keys and create one. A key can cover all your car parks
or only some; create one per system so you can revoke one without touching the
others. The key is shown once. The same page shows the full endpoint address and
each car park's site_id.
Two shapes, because two kinds of system exist. Send whichever one you actually have — we would rather have your real data than a guess dressed up as ours.
If you know how full you are right now
POST /functions/v1/site-occupancy-ingest
Authorization: Bearer pbk_<your key>
Content-Type: application/json
{ "free_spaces": 142, "total_spaces": 200,
"observed_at": "2026-09-22T09:14:00Z" }
| Field | Notes |
|---|---|
free_spaces |
Spaces currently free. Send this if you have it. |
total_spaces |
Capacity. Omit if we already have it recorded. |
band |
open / busy / nearly_full / full / closed — use when you have no numbers at all. |
observed_at |
Your clock. We store it separately from when we received it, so staleness is attributable. |
site_id |
Which car park. Required for keys created in Spaces (the API keys page lists each car park's site_id). |
Batch up to 100 readings as { "readings": [ … ] }.
Give free_spaces, a band, or both. A count with no capacity is rejected
rather than guessed at — "142 free" says nothing about fullness without knowing
of how many.
If you count vehicles in and out
Connect your gates in Spaces (Connect car parks → Connect a car park →
Cameras or barriers) and send each crossing to the hardware ingest endpoint
as a vehicle.count event with direction in or out — or, if every
entrance and exit has a number-plate camera, let Parksby count the
vehicle.entry and vehicle.exit events you already send. See Counting cars
under Hardware ingest.
Do not manufacture in/out events if your system only knows a total — use the occupancy endpoint above.
Keys
Create them yourself in Spaces → API keys, for all your car parks or only some. A key is shown once, stored only as a hash, and can be revoked at any time.
How often
Whatever matches reality — every minute for a live panel, every fifteen for a periodic export. Each reading expires after an hour, after which your site falls back to other sources rather than showing your number as current. We never display stale data as live.
Publisher API
For cities, national access points, transport authorities and operator networks that already publish parking data — you do not need to be on Parksby, own the car parks or take bookings through us. Send your car park list and your live counts in your own ids, in our JSON or in the standard you already use.
Getting a key. Write to contact@parksby.io with what you publish, where, and under which licence (or that you give us permission). We set you up as a publisher and send you one key. Your car parks are checked before they first appear on the map; after that, updates flow straight through. Your credit line is shown on every car park you send.
POST /functions/v1/site-occupancy-ingest
Authorization: Bearer pbk_<your publisher key>
Our JSON
{ "sites": [
{ "external_id": "P-0042", "name": "Station Garage",
"latitude": 50.8466, "longitude": 4.3528,
"site_type": "garage", "capacity": 400,
"address": "Rue Example 1, Brussels", "operator_label": "City Parking" } ],
"readings": [
{ "external_id": "P-0042", "free_spaces": 120, "total_spaces": 400,
"observed_at": "2026-10-08T15:00:00Z" } ] }
| Field | Notes |
|---|---|
external_id |
Your id for the car park. Readings refer to it; it never changes. |
name, latitude, longitude |
Required for a site. |
site_type |
garage, surface_lot or onstreet_zone. |
capacity |
Total spaces. Without it, a count cannot become Open / Busy / Full. |
free_spaces, total_spaces, band, observed_at |
As in the occupancy API above. |
Send sites when your list changes (or daily), and readings as often as
your counts change. A reading for an id you have not sent as a site yet is
reported back as unresolved.
FIWARE (NGSI v2 and NGSI-LD)
Send Smart Data Models entities — OffStreetParking, OnStreetParking and
ParkingSpot — as an entity, an array, or an Orion notification exactly as
your context broker sends it: point a subscription at the endpoint above with
your key in the Authorization header. We read id, name, location,
totalSpotNumber, availableSpotNumber, status and observedAt /
dateObserved. ParkingSpot bays are counted per refParkingSite.
DATEX II
Send a ParkingTablePublication and/or ParkingStatusPublication (v2 or v3)
as XML with Content-Type: application/xml. We read parkingRecord (id,
name, coordinates, capacity) and parkingRecordStatus (vacant and occupied
spaces, origin time, closed).
Limits
Up to 500 sites and 500 readings per request; 2 MB per body. Each reading is live for an hour. Keys are issued per publisher and can be paused at any time; data that is false or misleading is removed.
What drivers see
- Your latest reading, with how long ago it was taken.
- A reading counts as live for an hour; after that drivers aren't shown it as current.
- Occupancy you report never changes bookings. They are separate things.
Talk to us
For bulk imports, or if you're not sure which route fits your system, write to contact@parksby.io.
Webhook API (Parksby -> Your Server)
OutboundUse this when Parksby should notify your backend about booking and payment lifecycle events.
Parksby Outbound Webhook API
Version: 1.1
Direction: Parksby -> Your server
Protocol: HTTPS / REST
Authentication: HMAC-SHA256 signature header
Overview
Parksby webhooks tell your backend when something happens to a booking at one of your car parks: it is confirmed or paid, the car arrives or leaves, the booking is extended, runs late, ends or is cancelled.
Add endpoints in Spaces (spaces.parksby.io) under Developers → Webhooks. Each endpoint belongs to one car park and receives only that car park's events, for the event types you tick.
Endpoint (Your Server)
You provide an HTTPS URL, for example:
https://api.yourcompany.com/webhooks/parksby
Parksby sends POST requests to it.
Headers Sent By Parksby
| Header | Description |
|---|---|
Content-Type |
application/json |
X-Parksby-Timestamp |
Unix time (seconds) when the request was sent |
X-Parksby-Signature |
HMAC-SHA256 of the raw body, hex |
X-Parksby-Event-ID |
The event's UUID (same as event_id in the body) |
Event types
event_type |
In Spaces | When |
|---|---|---|
booking.activated |
Booking confirmed | A booking is confirmed and active |
payment.confirmed |
Payment confirmed | A driver's payment for a booking is confirmed |
booking.checked_in |
Car arrived | The booked car arrived (at the gate or checked in by staff) |
booking.checked_out |
Car left | The booked car left |
booking.extended |
Booking extended | The driver extended their booking |
booking.expiring_soon |
Booking ending soon | A booking is about to end |
booking.overdue |
Booking overdue | The booking has ended but the car has not left |
booking.completed |
Booking completed | The booking is finished |
booking.cancelled |
Booking cancelled | The booking was cancelled |
An endpoint with no event types ticked receives all of them.
Payload Shape
{
"event_id": "4c16c6dd-b2ca-4208-91d2-26eb2b4834f3",
"event_type": "booking.completed",
"occurred_at": "2026-03-20T13:00:00.000Z",
"data": {
"booking_id": "...",
"parking_id": "...",
"tenant_id": "..."
}
}
Field notes:
event_id: globally unique event identifier. Use it for idempotency.event_type: one of the event types above.occurred_at: ISO timestamp when the event occurred.data: event-specific fields. Every booking event carriesbooking_idandparking_id; most also carrytenant_id(the driver), and some add times, the plate or status. Ignore fields you do not use — more may be added.
Signature Verification
Parksby signs the raw JSON body with your endpoint's signing secret (shown in Spaces when you create the endpoint, and on its secret view).
signature = hex( HMAC-SHA256(webhook_secret, raw_body) )
Important:
- The signature covers the raw body bytes only.
X-Parksby-Timestampis not part of the signature. Use it to drop stale deliveries (recommended window: 5 minutes), and rely onevent_idto ignore replays.
Node.js Example
import crypto from "crypto";
function verifyParksbyWebhook(rawBody, signatureHeader, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
const received = String(signatureHeader ?? "");
// timingSafeEqual throws when lengths differ, so check first.
if (received.length !== expected.length) return false;
return crypto.timingSafeEqual(Buffer.from(expected, "utf8"), Buffer.from(received, "utf8"));
}
Python Example
import hmac
import hashlib
def verify_parksby_webhook(raw_body: bytes, signature_header: str, secret: str) -> bool:
expected = hmac.new(
secret.encode("utf-8"),
raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature_header or "")
Delivery Semantics
- Delivery is successful when your endpoint returns 2xx.
4xx(except429) is treated as a permanent failure (no retries).429,5xx, and network failures are retried with backoff.- Maximum attempts: 5.
Retry backoff after each failed attempt:
- after attempt 1: 2 minutes
- after attempt 2: 4 minutes
- after attempt 3: 8 minutes
- after attempt 4: 16 minutes
- attempt 5 is the last
Delivery status and the last response code are shown per endpoint in Spaces → Webhooks.
Idempotency Requirements
Your webhook consumer must be idempotent: a retry can deliver the same event twice.
Recommended approach:
- Read
event_idfrom the payload (orX-Parksby-Event-ID). - If already processed, return
200immediately. - Otherwise process it and store
event_idin your dedupe store.
Recommended Receiver Behavior
- Capture the raw body bytes.
- Verify the HMAC signature.
- Check the timestamp is recent.
- Parse JSON only after the signature passes.
- Check
event_idfor idempotency. - Process the event.
- Return
200quickly (do slow work after responding).
Security Checklist
- Keep the signing secret in a server-side secret manager or environment variable.
- Never log secrets.
- Use HTTPS only.
- Verify the signature on every request.
- Drop stale timestamps and already-seen
event_ids.
Troubleshooting
- Signature mismatch:
- Verify against the raw body bytes, not re-serialized JSON.
- Check you use the endpoint's current secret (after rotating it in Spaces, update your backend too).
- Nothing arrives:
- Check the endpoint is active (not paused) and belongs to the right car park.
- Check the event types you ticked.
- High retry counts:
- Check your endpoint's latency and non-2xx responses.
- Make sure the endpoint is publicly reachable with a valid TLS certificate.
- Duplicate deliveries:
- Expected under retries; enforce idempotency with
event_id.
- Expected under retries; enforce idempotency with
Hardware Ingest API (Hardware -> Parksby)
InboundUse this when cameras, barriers or counters at your gates send cars in and out to Parksby: to show drivers how full you are, and to let Parksby bookings through the barrier. Bookings are not required: counters and cameras work for occupancy alone, and a counter never opens a barrier.
Parksby Hardware Integration API
Version: 1.3 Protocol: HTTPS / REST Authentication: HMAC-SHA256
Overview
The Parksby Hardware Integration API lets the equipment at your gates — number-plate (ANPR) cameras, barrier controllers, loops and beams — send cars coming and going to Parksby. It does two jobs:
- Show drivers how full you are. Parksby counts cars in and out and shows your car park's availability in the app.
- Let Parksby bookings through your gate. A car with a Parksby booking or pass is matched by its plate, and Parksby tells your barrier to open.
When an event is received, Parksby:
- Authenticates the request cryptographically
- Counts it towards cars inside, if you chose to count
- Matches the vehicle plate against active bookings and passes
- Returns a barrier instruction synchronously in the HTTP response
All of this happens within a single request-response cycle. The hardware does not need to poll for decisions.
Prerequisites
Before any hardware can send events, set up the connection in Spaces (spaces.parksby.io):
- Connect the car park — Connect car parks → Connect a car park → Cameras or barriers at the gates, choose the car park and how cars should be counted
- Register your devices — add each camera, barrier or counter under the Devices tab; copy the assigned Device ID for your hardware configuration
- Copy the details for your installer — from the For your installer tab, copy the Integration ID and Signing Secret
- Switch the connection on — under Settings, turn Connection on and choose what happens at the gate:
- Only record — Parksby records cars (and counts them, if you chose to); it never opens the barrier
- Open the barrier for Parksby bookings — Parksby opens the barrier for cars with a Parksby booking or pass
Endpoint
POST /api/v1/ingest
This endpoint accepts hardware events. It is publicly reachable without a session cookie — authentication is handled via the request signature described below.
Production base URL: https://spaces.parksby.io
Production ingest URL: https://spaces.parksby.io/api/v1/ingest
Always send to the Spaces address above; it is the stable endpoint for hardware.
Authentication
Every request must be signed using HMAC-SHA256. Parksby verifies the signature before processing any event.
Signing algorithm
signature = hex( HMAC-SHA256( signing_secret, raw_body + "." + timestamp ) )
Where:
signing_secret— the secret from the For your installer tab in Spaces (treat it as a password; never expose it)raw_body— the exact JSON string you will send as the request body (byte-for-byte, no reformatting)timestamp— the ISO 8601 UTC timestamp you will send inX-Parksby-Timestamp"."— a literal period character as separator
The signature must be the lowercase hex-encoded digest.
Critical: The body used to compute the signature must be identical to the body transmitted over the wire. Do not re-serialize or reformat JSON after signing.
Required headers
| Header | Description |
|---|---|
X-Parksby-Integration-Id |
Your integration UUID |
X-Parksby-Timestamp |
Current UTC time in ISO 8601 format (e.g. 2026-03-15T10:00:00.000Z) |
X-Parksby-Signature |
Hex-encoded HMAC-SHA256 signature (see above) |
Content-Type |
Must be application/json |
Clock skew
Parksby enforces a ±5 minute clock skew tolerance. Requests with a timestamp outside this window are rejected. Ensure your hardware clock is synchronised (NTP recommended).
Secret rotation
When you rotate your signing secret in Spaces, the previous secret stays valid for 5 minutes, so you can deploy the new secret to your hardware without downtime.
Request body
{
"event_id": "a3f1c2d4-...",
"event_type": "vehicle.entry",
"event_time": "2026-03-15T10:00:00.000Z",
"device_id": "10d6a3b8-...",
"plate_number": "KDA 123A"
}
Required fields
| Field | Type | Description |
|---|---|---|
event_id |
string | Unique identifier for this event (UUID recommended). Used for deduplication — sending the same event_id twice is safe and idempotent. |
event_type |
string | One of vehicle.entry, vehicle.exit, vehicle.count or device.heartbeat |
event_time |
string | ISO 8601 UTC timestamp of when the event occurred on the hardware |
device_id |
string | The Device ID assigned by Parksby (copy in Spaces Devices tab) |
Optional fields
| Field | Type | Description |
|---|---|---|
plate_number |
string | Vehicle registration plate, for vehicle.entry and vehicle.exit (without it an event cannot be matched to a booking). Omit for heartbeats and counts. Spacing and letter case are normalised automatically. |
direction |
string | in or out, for vehicle.count. Optional if the device is registered with an entry or exit direction. |
seq |
integer | A counter your device increments with every event. Optional, but it lets Parksby notice events that never arrived. |
lane_id |
string | Identifier for the specific lane or gate, if your site has multiple |
confidence |
number | ANPR read confidence from 0 to 1 (e.g. 0.94 = 94%). Compared with the confidence threshold you set in Spaces (a percentage). |
image_url |
string | URL to the plate capture image, if your system stores images |
ocr_raw_text |
string | Raw OCR output before normalisation, for audit purposes |
metadata |
object | Any additional vendor-specific fields. Stored as-is for debugging. |
Event types
`device.heartbeat`
Sent periodically (recommended: every 60 seconds) to indicate the device is online. Parksby uses this to compute device health status shown in Spaces.
No plate matching is performed. The response is:
{
"status": "ok",
"event_id": "...",
"event_type": "device.heartbeat"
}
`vehicle.entry`
Sent when a vehicle is detected entering the facility. Parksby matches the plate against active bookings and passes and returns a barrier instruction.
`vehicle.exit`
Sent when a vehicle is detected leaving. Parksby finds the active session for this plate and applies site/payment policies before issuing a barrier decision.
`vehicle.count`
An anonymous crossing from a loop, beam or counter — no plate. Include "direction": "in" or "out" in the body (or register the device with an entry or exit direction). It is counted towards cars inside and never matched to a booking; the response always keeps the barrier closed.
Counting cars
Parksby turns your events into how full the car park is — what drivers see in the app. Choose how in Spaces (Cars inside → Count cars):
| Option | Counts | Use when |
|---|---|---|
| From the cameras or barriers | vehicle.entry (+1) and vehicle.exit (−1) |
Every entrance and exit has one, so each car is seen once each way |
| From a counter | vehicle.count with direction |
A loop, beam or counter reports crossings; cameras are only for bookings |
| Don't count | nothing | Some cars bypass the gates, or you already send free spaces with an occupancy key |
Never both: a car seen by a camera and a loop would count twice.
- Starting point. The count starts from the last known number: a head count your staff enter in Spaces (Cars inside now), or otherwise an empty car park at 3 am local time.
- Capacity. Drivers see how full you are only once Parksby knows how many spaces you have (set it on the parking in Spaces).
- Silence. If no counted events arrive for a day, drivers aren't shown a number — Parksby doesn't guess.
- Drift. Missed reads add up over a day. A quick head count now and then keeps it right.
Response
All responses are JSON. The HTTP status code indicates whether Parksby successfully received and processed the event — it does not indicate whether the barrier should be raised (use raise_barrier for that).
Success — `200 OK`
{
"status": "ok",
"event_id": "a3f1c2d4-...",
"event_type": "vehicle.entry",
"match_type": "matched_booking",
"match_reason": "Plate matched single active booking.",
"session_id": "b7e2a1f0-...",
"review_item_id": null,
"raise_barrier": true,
"barrier_action": "open"
}
Duplicate event — `200 OK`
If you send an event with an event_id Parksby has already processed, it is acknowledged without reprocessing:
{
"status": "duplicate",
"event_id": "a3f1c2d4-...",
"message": "Event already processed."
}
Retry contract (important)
If your hardware receives transport errors or HTTP 5xx responses, retry the same event using the same event_id.
- Retries with the same
event_idare idempotent. - Parksby deduplicates by
(integration_id, event_id). - Do not generate a new
event_idfor a retry of the same physical event.
Needs checking — `200 OK`
When Parksby cannot match a plate to a booking or pass, it records the event, adds it to Needs checking in Spaces for your staff, and keeps the barrier closed:
{
"status": "ok",
"event_id": "...",
"event_type": "vehicle.entry",
"match_type": "unmatched",
"match_reason": "No matching booking found.",
"session_id": null,
"review_item_id": "c9d3b2a1-...",
"raise_barrier": false,
"barrier_action": "deny"
}
A read below your confidence threshold is handled the same way but answers "status": "review" and "match_type": "low_confidence".
Counted car — `200 OK`
A vehicle.count is counted and never matched; the barrier stays closed:
{
"status": "ok",
"event_id": "...",
"event_type": "vehicle.count",
"direction": "in",
"raise_barrier": false,
"barrier_action": "deny"
}
Denied plate — `200 OK`
If the plate is on the site denylist:
{
"status": "denied",
"match_type": "denied",
"raise_barrier": false,
"barrier_action": "deny"
}
Barrier control
The raise_barrier boolean in every vehicle event response is the authoritative barrier command. Your hardware must act on it immediately and synchronously.
raise_barrier |
barrier_action |
Meaning |
|---|---|---|
true |
"open" |
Raise the barrier — vehicle is authorised |
false |
"deny" |
Keep the barrier closed |
raise_barrier is only ever true when:
- The gate is set to Open the barrier for Parksby bookings, AND
- For entry: the plate matched a booking, a pass, or the allowlist (or drive-in is enabled)
- For exit: the plate matched an active session, a pass, or the allowlist, and no deny policy applies
Exit safety gate for postpaid bookings
When Parksby opens the barrier, if an exit event matches a linked metered postpaid booking that is still unpaid, Parksby returns:
raise_barrier: falsebarrier_action: "deny"
The response match_reason explains that payment is required before barrier open.
When the gate is set to Only record, raise_barrier is always false; staff operate the barrier as they do today.
Match types
The match_type field in the response tells you how Parksby classified the event:
match_type |
Description |
|---|---|
matched_booking |
Plate matched exactly one active booking within the time window |
matched_session |
Exit matched an active session created at entry |
pass_match |
Plate has a valid parking pass for this site |
allowlist_match |
Plate is on the site allowlist |
drive_in_created |
No booking found; drive-in session created (requires site policy) |
ambiguous |
Plate matched several bookings — added to Needs checking |
unmatched |
No booking, session, or pass found for this plate |
denied |
Plate is on the site denylist |
low_confidence |
ANPR read confidence below the configured threshold |
Error responses
Errors return a non-2xx status.
| HTTP status | Error code | Cause |
|---|---|---|
400 |
missing_headers |
One or more required headers are absent |
400 |
invalid_timestamp |
X-Parksby-Timestamp is not a valid ISO 8601 string |
400 |
timestamp_skew |
Timestamp is more than ±5 minutes from server time |
400 |
missing_fields |
Required body fields are absent |
400 |
invalid_event_type |
event_type is not one of the allowed values |
400 |
invalid_event_time |
event_time is not a valid ISO 8601 string |
400 |
invalid_json |
Request body is not valid JSON |
400 |
missing_direction |
A vehicle.count has no direction and its device has none |
401 |
unknown_integration |
No integration found for the provided Integration ID |
401 |
integration_disabled |
The connection is switched off — switch it on in Spaces (Settings) |
401 |
invalid_signature |
HMAC signature verification failed |
401 |
unknown_device |
Device ID not registered under this integration |
403 |
device_quarantined |
Device has been quarantined — re-activate it in Spaces |
405 |
method_not_allowed |
Only POST is accepted |
500 |
storage_error |
Internal error storing the event |
500 |
normalization_error |
Internal error storing the normalized event |
500 |
ingest_unhandled_error |
Internal function/runtime error in ingest processing |
502 |
upstream_unreachable |
Spaces API proxy could not reach the upstream ingest function |
5xx |
upstream_non_json_error |
Upstream returned non-JSON error body through the Spaces API proxy |
Error response body:
{
"error": "error_code",
"message": "Human-readable description."
}
Stale events
If event_time is more than 10 minutes (or the stale threshold set in Spaces) before the server receives the request, the event is flagged as stale. It is still processed and stored, but is_stale: true is recorded on the event and visible in Spaces. This can occur if your hardware buffers events during connectivity loss.
Site policies
These are set per connected car park in Spaces (Settings → Advanced: plate-matching rules). The defaults apply until you change them:
| Policy | Default | Description |
|---|---|---|
booking_match_window_minutes |
60 | How far before and after event_time Parksby searches for a matching booking |
confidence_threshold |
0 (disabled) | Minimum ANPR confidence (0–100). Reads below this threshold are flagged for review. |
allow_drive_in |
false | Whether to let in cars without a booking when Parksby opens the barrier |
allow_auto_checkout |
true | Whether matched exits automatically end the booking |
stale_threshold_minutes |
10 | How old an event can be before it is flagged as stale (a stale event never opens the barrier) |
plate_allowlist |
[] |
Plates that bypass booking matching and are always granted access |
plate_denylist |
[] |
Plates that are always denied, regardless of bookings |
Code examples
Python (Raspberry Pi / edge device)
import hmac
import hashlib
import json
import time
import uuid
from datetime import datetime, timezone
import requests
PARKSBY_URL = "https://spaces.parksby.io/api/v1/ingest"
INTEGRATION_ID = "8d2244f1-dc10-4bdd-aaab-c62638928a18"
SIGNING_SECRET = "your-signing-secret"
DEVICE_ID = "10d6a3b8-ff84-4fa9-a5cc-9713b223ec56"
def send_event(event_type: str, plate: str = "") -> dict:
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z")
payload = {
"event_id": str(uuid.uuid4()),
"event_type": event_type,
"event_time": timestamp,
"device_id": DEVICE_ID,
"plate_number": plate,
}
# Sign: HMAC-SHA256(secret, body + "." + timestamp)
body = json.dumps(payload, separators=(",", ":"))
message = body + "." + timestamp
signature = hmac.new(
SIGNING_SECRET.encode(),
message.encode(),
hashlib.sha256
).hexdigest()
response = requests.post(
PARKSBY_URL,
data=body, # Send the exact body that was signed
headers={
"Content-Type": "application/json",
"X-Parksby-Integration-Id": INTEGRATION_ID,
"X-Parksby-Timestamp": timestamp,
"X-Parksby-Signature": signature,
},
timeout=5,
)
return response.json()
# Heartbeat
result = send_event("device.heartbeat")
print(result)
# Vehicle entry
result = send_event("vehicle.entry", plate="KDA 123A")
if result.get("raise_barrier"):
open_barrier() # your hardware control function
else:
keep_barrier_closed()
Note on
json.dumps: Useseparators=(",", ":")to avoid whitespace differences between signing and transmission. The body passed torequests.post(data=...)must be the exact same string used to compute the signature.
curl (testing)
Generate a fresh signature for the current time:
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
SECRET="your-signing-secret"
INTEGRATION_ID="8d2244f1-dc10-4bdd-aaab-c62638928a18"
DEVICE_ID="10d6a3b8-ff84-4fa9-a5cc-9713b223ec56"
BODY=$(printf '{"event_id":"%s","event_type":"vehicle.entry","event_time":"%s","device_id":"%s","plate_number":"KDA 123A"}' \
"$(uuidgen | tr '[:upper:]' '[:lower:]')" "$TIMESTAMP" "$DEVICE_ID")
SIGNATURE=$(echo -n "${BODY}.${TIMESTAMP}" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
curl -X POST https://spaces.parksby.io/api/v1/ingest \
-H "Content-Type: application/json" \
-H "X-Parksby-Integration-Id: $INTEGRATION_ID" \
-H "X-Parksby-Timestamp: $TIMESTAMP" \
-H "X-Parksby-Signature: $SIGNATURE" \
-d "$BODY"
Node.js
const crypto = require("crypto");
const PARKSBY_URL = "https://spaces.parksby.io/api/v1/ingest";
const INTEGRATION_ID = "8d2244f1-dc10-4bdd-aaab-c62638928a18";
const SIGNING_SECRET = "your-signing-secret";
const DEVICE_ID = "10d6a3b8-ff84-4fa9-a5cc-9713b223ec56";
async function sendEvent(eventType, plate = "") {
const timestamp = new Date().toISOString();
const payload = {
event_id: crypto.randomUUID(),
event_type: eventType,
event_time: timestamp,
device_id: DEVICE_ID,
plate_number: plate,
};
const body = JSON.stringify(payload);
const message = body + "." + timestamp;
const signature = crypto
.createHmac("sha256", SIGNING_SECRET)
.update(message)
.digest("hex");
const res = await fetch(PARKSBY_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Parksby-Integration-Id": INTEGRATION_ID,
"X-Parksby-Timestamp": timestamp,
"X-Parksby-Signature": signature,
},
body,
});
return res.json();
}
// Example usage
const result = await sendEvent("vehicle.entry", "KDA 123A");
console.log(result);
if (result.raise_barrier) {
openBarrier(); // your hardware control
}
Testing
Spaces includes a test tool on the Test tab of each connected car park. It sends a signed heartbeat, car arriving, car leaving or car counted event from one of your registered devices and shows the raw response — the quickest way to check the connection before deploying hardware. The connection must be switched on first.
For end-to-end testing with real plate events, use the curl script above with a fresh timestamp. If you receive a timestamp_skew error, verify that your system clock is correct (date -u on Linux/macOS).
Security considerations
- Store your signing secret in environment variables or a secrets manager — never in source code
- Rotate your secret periodically in Spaces (For your installer); the grace window ensures zero-downtime rotation
- The
device_idin every event is verified against your integration — events from unregistered or foreign devices are rejected