NewParksby is live! Download the app!
</>Developer docsv1

Integrate with parksby

Two integration directions: outbound webhooks from Parksby to your backend, and inbound hardware ingest from cameras, barriers, and sensors into Parksby.

  • Webhook signatures with HMAC-SHA256
  • Hardware ingest on /api/v1/ingest
  • Idempotency with event_id
  • Production retry semantics
Integration quickstart
Choose your integration direction.
  1. 1If you need outbound events, configure webhook endpoint URLs in Spaces.
  2. 2If you need inbound device events, provision integration credentials and device IDs.
  3. 3Implement signature verification and idempotency on both sides.
  4. 4Run staged dry-runs and real-world drive tests before production rollout.

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.0
Direction: Parksby -> Your server
Protocol: HTTPS / REST
Authentication: HMAC-SHA256 signature header


Overview

Parksby webhooks notify your backend when important platform events happen (bookings, payments, session updates, reviews, etc.).

You configure one or more webhook endpoints in the dashboard. Parksby then POSTs event payloads to those URLs.


Endpoint (Your Server)

You provide the endpoint URL in the dashboard, for example:

https://api.yourcompany.com/webhooks/parksby

Parksby sends POST requests to this endpoint.


Headers Sent By Parksby

Header Description
Content-Type application/json
X-Parksby-Timestamp Unix timestamp (seconds) when request was signed
X-Parksby-Signature HMAC-SHA256 signature (hex)
X-Parksby-Event-ID Unique Parksby event UUID

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 for idempotency.
  • event_type: domain event name from Parksby.
  • occurred_at: ISO timestamp when event occurred.
  • data: event-specific payload object.

Signature Verification

Parksby signs the raw JSON body using your webhook secret.

signature = hex( HMAC-SHA256(webhook_secret, raw_body) )

Important:

  • Signature input is raw body bytes only.
  • X-Parksby-Timestamp is provided for replay protection and freshness checks.
  • Reject requests with stale timestamps (recommended window: <= 5 minutes).

Node.js Example

import crypto from "crypto";

function verifyParksbyWebhook(rawBody, signatureHeader, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(expected, "utf8"),
    Buffer.from(signatureHeader ?? "", "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 considered successful when your endpoint returns 2xx.
  • 4xx (except 429) is treated as permanent failure (no retries).
  • 429, 5xx, and network/transport failures are retried with backoff.
  • Maximum attempts: 5.

Retry backoff schedule uses exponential minutes with cap:

  • attempt 1 retry: 2 minutes
  • attempt 2 retry: 4 minutes
  • attempt 3 retry: 8 minutes
  • attempt 4 retry: 16 minutes
  • capped at 60 minutes

Idempotency Requirements

Your webhook consumer must be idempotent.

Recommended approach:

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

Recommended Receiver Behavior

  1. Capture raw body bytes.
  2. Verify timestamp freshness.
  3. Verify HMAC signature.
  4. Parse JSON only after signature passes.
  5. Idempotency check on event_id.
  6. Process event.
  7. Return 200 quickly.

Security Checklist

  • Keep webhook secret in server-side secret manager / env var.
  • Never log full secrets.
  • Use HTTPS only.
  • Enforce signature verification for every request.
  • Enforce timestamp freshness window to reduce replay risk.

Troubleshooting

  • Signature mismatch:
    • Ensure you verify against raw body bytes, not re-serialized JSON.
    • Ensure correct secret (rotate safely in dashboard and backend together).
  • High retry counts:
    • Check your endpoint latency and non-2xx responses.
    • Ensure endpoint is publicly reachable and TLS-valid.
  • Duplicate deliveries:
    • Expected under retries; enforce idempotency with event_id.

Hardware Ingest API (Hardware -> Parksby)

Inbound

Use this when on-site hardware sends ANPR, entry, exit, and heartbeat events into Parksby.

At a glance

  • POST signed event deliveries to /api/v1/ingest
  • Include integration_id + device_id
  • Use signed headers and consistent JSON bodies
  • Deduplicate + safely retry using event_id

Parksby Hardware Integration API

Version: 1.1 Protocol: HTTPS / REST Authentication: HMAC-SHA256


Overview

The Parksby Hardware Integration API allows parking hardware — ANPR cameras, barrier controllers, and loop sensors — to communicate vehicle entry and exit events to the Parksby platform in real time.

When an event is received, Parksby:

  • Authenticates the request cryptographically
  • Matches the vehicle plate against active bookings and passes
  • Creates or updates an access session
  • Returns a barrier control 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, you must complete the following steps in the Parksby dashboard:

  1. Create an integration — navigate to Integrations and create a new integration for your parking site
  2. Enable the integration — toggle Integration Status to enabled and save
  3. Register your devices — add each physical device (camera, barrier, sensor) under the Devices tab; copy the assigned Device ID for use in your hardware configuration
  4. Copy your credentials — from the Credentials tab, copy your Integration ID and Signing Secret
  5. Set your automation mode — under Configuration, choose the mode appropriate for your site:
    • Notify-only — Parksby records events and notifies staff; no barrier commands are issued
    • Assisted — Parksby matches events and flags anomalies; staff make final decisions
    • Full automation — Parksby issues barrier open/close commands autonomously

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

Use the Spaces dashboard domain endpoint above for hardware integrations. Do not call Supabase edge URLs directly from 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 Credentials tab (treat 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 from the dashboard, the previous secret remains valid for a configurable grace period (default: 5 minutes). This allows you to deploy the new secret to 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, 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 from the dashboard Devices tab)

Optional fields

Field Type Description
plate_number string Vehicle registration plate. Required for vehicle.entry and vehicle.exit events. Omit or use "" for heartbeats. Spaces and case are normalised automatically.
lane_id string Identifier for the specific lane or gate, if your site has multiple
confidence number ANPR read confidence as a decimal (e.g. 0.94 = 94%). Used by the confidence threshold policy.
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 on the dashboard.

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.


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.

Review required — `200 OK`

When Parksby cannot confidently match an event (e.g. unmatched plate, low ANPR confidence), it logs a review item for an operator and returns:

{
  "status": "review",
  "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"
}

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 integration mode is Full automation, 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

In full automation mode, 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.

In Notify-only or Assisted modes, raise_barrier is always false. Staff receive notifications and operate barriers manually.


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 multiple bookings — sent for operator review
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
401 unknown_integration No integration found for the provided Integration ID
401 integration_disabled Integration exists but is disabled — enable it from the dashboard
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 from the dashboard
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 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 the dashboard. This can occur if your hardware buffers events during connectivity loss.


Site policies

The following policies can be configured per integration from the dashboard:

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 accept unbooked vehicles in full automation mode
allow_auto_checkout true Whether matched exits automatically trigger booking checkout from hardware events
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

The Parksby dashboard includes a built-in test tool on the Test tab of each integration. It sends a signed device.heartbeat event using your first registered device and displays the raw response. This is the quickest way to verify your integration is correctly configured before deploying hardware.

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 from the Integrations dashboard; 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