NewNow in 20+ countries — see where
</>Developer docsv1

Integrate with parksby

For car park operators and their installers. Put your car parks on Parksby and send how full they are, connect cameras and barriers so Parksby bookings get through your gate, or receive webhooks when bookings change. Cities and networks that already publish parking data can send it in their own format.

  • List car parks — no booking arrangement needed
  • Occupancy API, or counting at the gate
  • Webhook signatures with HMAC-SHA256
  • FIWARE, DATEX II or our JSON for city and network feeds
Integration quickstart
Start from what you have.
  1. 1Your system knows free spaces: create an occupancy key in Spaces → API keys.
  2. 2Cameras, barriers or counters: Connect a car park in Spaces, add devices, give your installer the details.
  3. 3Want booking events in your backend: add a webhook endpoint in Spaces → Webhooks.
  4. 4A city, national or network feed: write to contact@parksby.io for a publisher key.
  5. 5Sign every request, retry with the same event_id, and use the Test tab before going live.

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)

Outbound

Use this when Parksby should notify your backend about booking and payment lifecycle events.

At a glance

  • Signed deliveries with HMAC-SHA256
  • Timestamp + signature + event-id headers
  • Verify raw request bytes (no JSON re-serialization)
  • Retry-safe handling via idempotency

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 carries booking_id and parking_id; most also carry tenant_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-Timestamp is not part of the signature. Use it to drop stale deliveries (recommended window: 5 minutes), and rely on event_id to 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 (except 429) 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:

  1. Read event_id from the payload (or X-Parksby-Event-ID).
  2. If already processed, return 200 immediately.
  3. Otherwise process it and store event_id in your dedupe store.

  1. Capture the raw body bytes.
  2. Verify the HMAC signature.
  3. Check the timestamp is recent.
  4. Parse JSON only after the signature passes.
  5. Check event_id for idempotency.
  6. Process the event.
  7. Return 200 quickly (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.

Hardware Ingest API (Hardware -> Parksby)

Inbound

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

At a glance

  • POST signed event deliveries to /api/v1/ingest
  • Header X-Parksby-Integration-Id; device_id in the body
  • Sign body + "." + timestamp with your secret
  • Deduplicate + safely retry using event_id

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):

  1. 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
  2. Register your devices — add each camera, barrier or counter under the Devices tab; copy the assigned Device ID for your hardware configuration
  3. Copy the details for your installer — from the For your installer tab, copy the Integration ID and Signing Secret
  4. 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 in X-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_id are idempotent.
  • Parksby deduplicates by (integration_id, event_id).
  • Do not generate a new event_id for 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: false
  • barrier_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: Use separators=(",", ":") to avoid whitespace differences between signing and transmission. The body passed to requests.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_id in every event is verified against your integration — events from unregistered or foreign devices are rejected