Developer API Reference

v1.0 REST API

Overview & Architecture

The Parkulo Driver & Agent API enables native mobile clients, in-dash vehicle infotainment systems, and autonomous AI agents to query live parking facilities, register vehicles, and execute autonomous parking transactions without interactive payment prompts.

Off-Session Vaulting

Card credentials are pre-vaulted in Stripe. Transactions run programmatically in the background without user interaction.

Idempotent Execution

Guarantees safe network retries via strict Idempotency-Key headers to avoid duplicate charges.

Autonomous Guardrails

Caps what an agent can spend starting a session at $30.00 and blocks a second active session for the same vehicle at the same lot.

Authentication

All requests to protected endpoints must provide your driver authorization token via the HTTP Authorization header using standard Bearer formatting. Get a token by verifying an SMS code below (add an optional token_name to label it), or create one under AI Agents on your driver dashboard. Tokens start with pt_. Your dashboard lists every token by name and lets you revoke each one.

HTTP Authorization Header
Authorization: Bearer <YOUR_DRIVER_TOKEN>
POST /api/v1/auth/request-code

Requests a 6-digit OTP verification code sent via SMS to a 10-digit US telephone number. A new number gets a new driver account. Codes are valid for 15 minutes; asking again within that time resends the same code. Each number can get 3 new codes every 3 hours.

REQUEST
curl -X POST https://parkulo.com/api/v1/auth/request-code \
  -H "Content-Type: application/json" \
  -d '{"phone": "5551234567"}'
import requests

url = "https://parkulo.com/api/v1/auth/request-code"
payload = {"phone": "5551234567"}
response = requests.post(url, json=payload)
print(response.json())
const response = await fetch("https://parkulo.com/api/v1/auth/request-code", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ phone: "5551234567" })
});
const data = await response.json();
console.log(data);
EXPECTED RESPONSE 200 OK
{
  "status": "code_sent",
  "user_id": "94b81ef0-4d56-42ac-bf7d-9477bcf4d10e"
}
POST /api/v1/auth/verify-code

Exchanges the 6-digit SMS code for an authenticated driver Bearer token. token_name is optional (up to 60 characters) and is how the token appears on your dashboard; it defaults to "API sign-in". A wrong or expired code returns 401.

REQUEST
curl -X POST https://parkulo.com/api/v1/auth/verify-code \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "94b81ef0-4d56-42ac-bf7d-9477bcf4d10e",
    "code": "482019",
    "token_name": "My car"
  }'
import requests

url = "https://parkulo.com/api/v1/auth/verify-code"
payload = {
    "user_id": "94b81ef0-4d56-42ac-bf7d-9477bcf4d10e",
    "code": "482019",
    "token_name": "My car"
}
response = requests.post(url, json=payload)
print(response.json())
const response = await fetch("https://parkulo.com/api/v1/auth/verify-code", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    user_id: "94b81ef0-4d56-42ac-bf7d-9477bcf4d10e",
    code: "482019",
    token_name: "My car"
  })
});
const data = await response.json();
console.log(data);
EXPECTED RESPONSE 200 OK
{
  "status": "authenticated",
  "token": "pt_Xq3vN8kL2mR7tY1wZ5cB9dF4gH6jK0pS",
  "token_type": "Bearer"
}

Vehicle Registry

Manage vehicles and license plates associated with the authenticated driver account.

GET /api/v1/vehicles

Retrieves all vehicles registered to your account.

REQUEST
curl -X GET https://parkulo.com/api/v1/vehicles \
  -H "Authorization: Bearer <YOUR_DRIVER_TOKEN>"
import requests

url = "https://parkulo.com/api/v1/vehicles"
headers = {"Authorization": "Bearer <YOUR_DRIVER_TOKEN>"}
response = requests.get(url, headers=headers)
print(response.json())
const response = await fetch("https://parkulo.com/api/v1/vehicles", {
  headers: { "Authorization": "Bearer <YOUR_DRIVER_TOKEN>" }
});
const data = await response.json();
console.log(data);
EXPECTED RESPONSE 200 OK
{
  "vehicles": [
    {
      "vehicle_id": "b8e5c1d3-7a2f-4e90-9c61-3f0d2a8e5b47",
      "user_id": "94b81ef0-4d56-42ac-bf7d-9477bcf4d10e",
      "car_name": "Model 3 Sedan",
      "plate_number": "7XYZ890",
      "plate_state": "CA",
      "is_placeholder": 0
    }
  ]
}
POST /api/v1/vehicles

Registers a new vehicle. car_name, plate_number and plate_state are all required; spaces and hyphens are removed from the plate. If you already saved that plate, its existing vehicle_id is returned.

REQUEST
curl -X POST https://parkulo.com/api/v1/vehicles \
  -H "Authorization: Bearer <YOUR_DRIVER_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "car_name": "Cybercab 01",
    "plate_number": "AUTON1",
    "plate_state": "TX"
  }'
import requests

url = "https://parkulo.com/api/v1/vehicles"
headers = {
    "Authorization": "Bearer <YOUR_DRIVER_TOKEN>",
    "Content-Type": "application/json"
}
payload = {
    "car_name": "Cybercab 01",
    "plate_number": "AUTON1",
    "plate_state": "TX"
}
response = requests.post(url, headers=headers, json=payload)
print(response.json())
const response = await fetch("https://parkulo.com/api/v1/vehicles", {
  method: "POST",
  headers: {
    "Authorization": "Bearer <YOUR_DRIVER_TOKEN>",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    car_name: "Cybercab 01",
    plate_number: "AUTON1",
    plate_state: "TX"
  })
});
const data = await response.json();
console.log(data);
EXPECTED RESPONSE 201 CREATED
{
  "status": "created",
  "vehicle_id": "4c8310ba-d52f-4b1e-8a7c-61e9f03d2c58"
}
DELETE /api/v1/vehicles/<vehicle_id>

Removes one of your vehicles. A vehicle that isn't on your account returns 404.

REQUEST
curl -X DELETE https://parkulo.com/api/v1/vehicles/4c8310ba-d52f-4b1e-8a7c-61e9f03d2c58 \
  -H "Authorization: Bearer <YOUR_DRIVER_TOKEN>"
import requests

url = "https://parkulo.com/api/v1/vehicles/4c8310ba-d52f-4b1e-8a7c-61e9f03d2c58"
headers = {"Authorization": "Bearer <YOUR_DRIVER_TOKEN>"}
response = requests.delete(url, headers=headers)
print(response.json())
const response = await fetch("https://parkulo.com/api/v1/vehicles/4c8310ba-d52f-4b1e-8a7c-61e9f03d2c58", {
  method: "DELETE",
  headers: { "Authorization": "Bearer <YOUR_DRIVER_TOKEN>" }
});
const data = await response.json();
console.log(data);
EXPECTED RESPONSE 200 OK
{
  "status": "deleted"
}

Facility Rates & Quotes

Inspect rates, geolocation coordinates, and calculate exact fee totals before booking. Note that <lot_id> accepts either the full facility UUID or the 6-digit lot code printed on signage.

GET /api/v1/lots/<lot_id>

Returns lot details, geolocation coordinates (null if the address couldn't be located), and rates. rate_periods lists any time-of-day rates that replace price_per_hour during the listed days and times; it is empty when the lot has one hourly rate. No token is needed.

REQUEST
curl -X GET https://parkulo.com/api/v1/lots/9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d
import requests

url = "https://parkulo.com/api/v1/lots/9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"
response = requests.get(url)
print(response.json())
const response = await fetch("https://parkulo.com/api/v1/lots/9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d");
const data = await response.json();
console.log(data);
EXPECTED RESPONSE 200 OK
{
  "lot_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
  "lot_name": "Downtown Plaza Deck",
  "address": "100 Main St, Austin, TX 78701",
  "min_fee": 5.0,
  "price_per_hour": 2.0,
  "daily_max": 20.0,
  "rate_periods": [
    {
      "name": "Evenings",
      "days": ["Mon", "Tue", "Wed", "Thu", "Fri"],
      "start_time": "17:00",
      "end_time": "22:00",
      "price_per_hour": 3.0
    }
  ],
  "latitude": 30.2672,
  "longitude": -97.7431
}
GET /api/v1/lots/<lot_id>/quote?hours=2.5

Prices parking that starts now. The first hour costs min_fee; after that each half hour costs half the hourly rate in effect when it begins (including time-of-day rates), and each 24 hours is capped at daily_max. service_fee is the convenience fee plus card processing (2.9% + 30¢ of the total). sales_tax is 8.25% of parking plus the convenience fee at Texas lots, and 0 elsewhere. hours defaults to 1. No token is needed.

REQUEST
curl -X GET "https://parkulo.com/api/v1/lots/9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d/quote?hours=2.5"
import requests

url = "https://parkulo.com/api/v1/lots/9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d/quote"
params = {"hours": 2.5}
response = requests.get(url, params=params)
print(response.json())
const response = await fetch("https://parkulo.com/api/v1/lots/9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d/quote?hours=2.5");
const data = await response.json();
console.log(data);
EXPECTED RESPONSE 200 OK
{
  "lot_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
  "hours": 2.5,
  "parking_charge": 8.00,
  "service_fee": 1.33,
  "sales_tax": 0.00,
  "total_amount": 9.33,
  "currency": "USD"
}

Autonomous Agent Session Execution

Initiates a live session programmatically without manual intervention. Charges are billed off-session to the payment method vaulted in the driver account's profile.

Prerequisites: The driver account must have an active payment card vaulted via the Driver Portal Billing settings.
POST /api/v1/agent/sessions/start

Initiates a session, charges the first saved card off-session, and activates enforcement clearance. The total is the same as a quote for the same lot and hours. Hourly parking isn't sold while an event is running at the lot: the request returns 409 with the event's name, end time and event_url, where the driver can buy an event pass.

Parameter Type Required Description
Idempotency-Key Header Yes Unique UUID string preventing duplicate charges during network retries.
lot_id String Yes The UUID or 6-digit text code of the destination facility.
vehicle_id String Yes The UUID of the vehicle being parked. It must be one of your vehicles, or the request returns 404.
hours Number No Requested parking duration in hours. Defaults to 1.
max_authorized_spend Number No Most the agent may spend, including fees and sales tax. Defaults to and can't exceed $30.00. If the total is higher, the request fails with 402 and nothing is charged.
REQUEST
curl -X POST https://parkulo.com/api/v1/agent/sessions/start \
  -H "Authorization: Bearer <YOUR_DRIVER_TOKEN>" \
  -H "Idempotency-Key: trip_sess_9021a8c-9821" \
  -H "Content-Type: application/json" \
  -d '{
    "lot_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
    "vehicle_id": "b8e5c1d3-7a2f-4e90-9c61-3f0d2a8e5b47",
    "hours": 2,
    "max_authorized_spend": 15.00
  }'
import uuid
import requests

url = "https://parkulo.com/api/v1/agent/sessions/start"
headers = {
    "Authorization": "Bearer <YOUR_DRIVER_TOKEN>",
    "Idempotency-Key": f"agent-{uuid.uuid4().hex}",
    "Content-Type": "application/json"
}
payload = {
    "lot_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",  # Full UUID or 6-digit sign code
    "vehicle_id": "b8e5c1d3-7a2f-4e90-9c61-3f0d2a8e5b47",
    "hours": 2,
    "max_authorized_spend": 15.00
}
response = requests.post(url, headers=headers, json=payload)
print(response.json())
const response = await fetch("https://parkulo.com/api/v1/agent/sessions/start", {
  method: "POST",
  headers: {
    "Authorization": "Bearer <YOUR_DRIVER_TOKEN>",
    "Idempotency-Key": `agent-${crypto.randomUUID()}`,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    lot_id: "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d", // Full UUID or 6-digit sign code
    vehicle_id: "b8e5c1d3-7a2f-4e90-9c61-3f0d2a8e5b47",
    hours: 2,
    max_authorized_spend: 15.00
  })
});
const data = await response.json();
console.log(data);
EXPECTED RESPONSE 201 CREATED
{
  "status": "active",
  "session_id": "31bdf721-a010-441f-82bb-70e17d23a7bb",
  "facility_name": "Downtown Plaza Deck",
  "start_time": "2026-10-03T11:30:00-05:00",
  "end_time": "2026-10-03T13:30:00-05:00",
  "total_charge": 8.30,
  "payment_intent_id": "pi_3N9ab81kLz81"
}

Session Extensions & Early Release

Extend active sessions or release a space when vehicle departures are detected.

POST /api/v1/agent/sessions/<session_id>/extend

Adds time (extra_hours, default 1) and charges the difference, plus fees and any sales tax, to the card the session was paid with. The new price is figured from the session's original start, so the daily maximum still applies. Time past the daily maximum is free, and a difference under Stripe's 50¢ minimum is added for free now and charged with the next extension. extension_count counts the extensions that were charged. Extensions aren't limited by max_authorized_spend. If the session has no saved card, or the card is declined, the request returns 400 and the session is unchanged.

REQUEST
curl -X POST https://parkulo.com/api/v1/agent/sessions/31bdf721-a010-441f-82bb-70e17d23a7bb/extend \
  -H "Authorization: Bearer <YOUR_DRIVER_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"extra_hours": 1.0}'
import requests

url = "https://parkulo.com/api/v1/agent/sessions/31bdf721-a010-441f-82bb-70e17d23a7bb/extend"
headers = {
    "Authorization": "Bearer <YOUR_DRIVER_TOKEN>",
    "Content-Type": "application/json"
}
payload = {"extra_hours": 1.0}
response = requests.post(url, headers=headers, json=payload)
print(response.json())
const response = await fetch("https://parkulo.com/api/v1/agent/sessions/31bdf721-a010-441f-82bb-70e17d23a7bb/extend", {
  method: "POST",
  headers: {
    "Authorization": "Bearer <YOUR_DRIVER_TOKEN>",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({ extra_hours: 1.0 })
});
const data = await response.json();
console.log(data);
EXPECTED RESPONSE 200 OK
{
  "status": "extended",
  "session_id": "31bdf721-a010-441f-82bb-70e17d23a7bb",
  "new_end_time": "2026-10-03T14:30:00-05:00",
  "total_hours": 3.0,
  "extension_count": 1
}
POST /api/v1/agent/sessions/<session_id>/stop

Ends the session now, for example once departure sensors trigger. Ending early doesn't refund unused time.

REQUEST
curl -X POST https://parkulo.com/api/v1/agent/sessions/31bdf721-a010-441f-82bb-70e17d23a7bb/stop \
  -H "Authorization: Bearer <YOUR_DRIVER_TOKEN>"
import requests

url = "https://parkulo.com/api/v1/agent/sessions/31bdf721-a010-441f-82bb-70e17d23a7bb/stop"
headers = {"Authorization": "Bearer <YOUR_DRIVER_TOKEN>"}
response = requests.post(url, headers=headers)
print(response.json())
const response = await fetch("https://parkulo.com/api/v1/agent/sessions/31bdf721-a010-441f-82bb-70e17d23a7bb/stop", {
  method: "POST",
  headers: { "Authorization": "Bearer <YOUR_DRIVER_TOKEN>" }
});
const data = await response.json();
console.log(data);
EXPECTED RESPONSE 200 OK
{
  "status": "ended",
  "session_id": "31bdf721-a010-441f-82bb-70e17d23a7bb",
  "terminated_at": "2026-10-03T14:02:11-05:00"
}

status is ended, or deleted if the session was never paid.

GET /api/v1/sessions/active

Lists your sessions that haven't ended yet, with the lot, vehicle and rates for each. Use it to find a session_id to extend or stop. Responses include more fields than shown here.

REQUEST
curl -X GET https://parkulo.com/api/v1/sessions/active \
  -H "Authorization: Bearer <YOUR_DRIVER_TOKEN>"
EXPECTED RESPONSE 200 OK
{
  "active_sessions": [
    {
      "session_id": "31bdf721-a010-441f-82bb-70e17d23a7bb",
      "lot_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
      "lot_name": "Downtown Plaza Deck",
      "address": "100 Main St, Austin, TX 78701",
      "vehicle_id": "b8e5c1d3-7a2f-4e90-9c61-3f0d2a8e5b47",
      "car_name": "Model 3 Sedan",
      "plate_number": "7XYZ890",
      "plate_state": "CA",
      "start_time": "2026-10-03T11:30:00-05:00",
      "end_time": "2026-10-03T14:30:00-05:00",
      "hours": 3.0,
      "total_charge": 9.0,
      "payment_status": "paid",
      "extension_count": 1,
      "event_id": null,
      "event_name": null
    }
  ]
}

total_charge here is the parking charge without fees.

POST /api/v1/sessions/<session_id>/end

Same as /stop, but returns only {"status": "ended"}.

Guardrails & Error Handling

The API returns standard HTTP status codes along with descriptive JSON error payloads:

Status Code Condition Example Response
400 Bad Request Missing Idempotency-Key header, spend limit over $30.00, invalid numbers, missing vehicle fields, no card ever saved on the account, or an extension that couldn't be charged. {"error": "Autonomous spend authorization exceeds safety ceiling of $30.00"}
401 Unauthorized Invalid, revoked or missing token, or a wrong sign-in code. {"error": "Unauthorized API token"}
402 Payment Required No saved card, charge declined, or the total exceeds max_authorized_spend. {"error": "Quoted total exceeds max_authorized_spend", "quoted_total": 35.00, "authorized_limit": 30.0}
409 Conflict An active session already exists for the same vehicle at this facility, or an event is running there. {"error": "An active parking session is already underway for this vehicle at this facility"}
404 Not Found The lot is unknown or inactive, or the vehicle or session isn't yours. {"error": "Target parking session not found"}
429 Too Many Requests Sign-in code limit reached (3 new codes per phone number every 3 hours). {"error": "Rate limit exceeded. Please try again in 3 hours."}
Lot Owner & Integrator API

Owner API & LPR Camera Integration

The Lot Owner REST API allows parking operators, facility managers, and automated License Plate Recognition (LPR) camera software to query properties, stream live session data, and validate vehicle authorization in real time.

Managing API Keys: You can create and revoke named API keys anytime inside your Owner Dashboard → API Keys tab.
Authentication Headers

Pass your provisioned API key in each request using either standard Bearer token authorization or the X-API-Key header:

HTTP Authorization Headers
Authorization: Bearer pk_live_<YOUR_API_KEY>
# Or alternatively:
X-API-Key: pk_live_<YOUR_API_KEY>

Driver tokens (pt_) don't work on owner endpoints and return 401. Requests for a lot you don't own return 403, and an unknown lot returns 404.

Property Catalog

Query lots associated with your owner account and inspect facility pricing and utilization stats.

GET /api/v1/owner/lots

Lists all properties under your ownership, including archived ones (is_active: 0), along with rates, active vehicle counts, and QR links. text_id is the 6-digit lot code on your signs. Responses include more fields than shown here.

REQUEST
curl -X GET https://parkulo.com/api/v1/owner/lots \
  -H "Authorization: Bearer <YOUR_OWNER_API_KEY>"
import requests

url = "https://parkulo.com/api/v1/owner/lots"
headers = {"Authorization": "Bearer <YOUR_OWNER_API_KEY>"}
response = requests.get(url, headers=headers)
print(response.json())
const response = await fetch("https://parkulo.com/api/v1/owner/lots", {
  headers: { "Authorization": "Bearer <YOUR_OWNER_API_KEY>" }
});
const data = await response.json();
console.log(data);
EXPECTED RESPONSE 200 OK
{
  "total": 1,
  "lots": [
    {
      "lot_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
      "lot_name": "Downtown Deck",
      "text_id": "613515",
      "address": "100 Main St, Austin, TX 78701",
      "street_address": "100 Main St",
      "city": "Austin",
      "state": "TX",
      "zip_code": "78701",
      "is_active": 1,
      "min_fee": 5.0,
      "price_per_hour": 2.0,
      "daily_max": 20.0,
      "violation_fine": 50.0,
      "is_tenant_enabled": 1,
      "monthly_rate": 40.0,
      "active_count": 8,
      "last_24h_count": 31,
      "total_sessions": 1402,
      "rolling_30d_net": 1240.50,
      "net_violation_revenue": 135.00,
      "qr_code_url": "https://parkulo.com/park/9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"
    }
  ]
}
GET /api/v1/owner/lots/<lot_id>

Returns the same lot fields as above plus usage analytics: the busiest days, the busiest start hours, and the most frequent vehicles.

REQUEST
curl -X GET https://parkulo.com/api/v1/owner/lots/9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d \
  -H "Authorization: Bearer <YOUR_OWNER_API_KEY>"
import requests

url = "https://parkulo.com/api/v1/owner/lots/9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"
headers = {"Authorization": "Bearer <YOUR_OWNER_API_KEY>"}
response = requests.get(url, headers=headers)
print(response.json())
const response = await fetch("https://parkulo.com/api/v1/owner/lots/9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d", {
  headers: { "Authorization": "Bearer <YOUR_OWNER_API_KEY>" }
});
const data = await response.json();
console.log(data);
EXPECTED RESPONSE 200 OK
{
  "lot": {
    "lot_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
    "lot_name": "Downtown Deck",
    "text_id": "613515",
    "min_fee": 5.0,
    "price_per_hour": 2.0,
    "daily_max": 20.0,
    "active_count": 8,
    "rolling_30d_net": 1240.50,
    "effective_platform_fee": 0.75
  },
  "analytics": {
    "peak_days": [{"day": "Friday", "count": 212}],
    "peak_hours": [{"hour": "08", "count": 97}],
    "frequent": [{"vehicle_plate_number": "ABC1234", "vehicle_car_name": "Tesla Model 3", "count": 14}]
  }
}

Global Session Activity

Retrieves paid parking sessions and event passes across all properties in your portfolio, newest first.

GET /api/v1/owner/sessions

Accepts query filters for real-time occupancy monitoring and financial reconciliations.

Query Param Type Default Description
status String all Options: active (currently parked), past (ended; expired and ended also work), or all.
lot_id String None Optional UUID or 6-digit text code filter.
limit Integer 100 Number of sessions returned (Max: 500).
REQUEST
curl -X GET "https://parkulo.com/api/v1/owner/sessions?status=active&limit=50" \
  -H "Authorization: Bearer <YOUR_OWNER_API_KEY>"
import requests

url = "https://parkulo.com/api/v1/owner/sessions"
headers = {"Authorization": "Bearer <YOUR_OWNER_API_KEY>"}
params = {"status": "active", "limit": 50}
response = requests.get(url, headers=headers, params=params)
print(response.json())
const response = await fetch("https://parkulo.com/api/v1/owner/sessions?status=active&limit=50", {
  headers: { "Authorization": "Bearer <YOUR_OWNER_API_KEY>" }
});
const data = await response.json();
console.log(data);
EXPECTED RESPONSE 200 OK
{
  "status_filter": "active",
  "count": 1,
  "sessions": [
    {
      "session_id": "31bdf721-a010-441f-82bb-70e17d23a7bb",
      "lot_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
      "lot_name": "Downtown Deck",
      "user_id": "94b81ef0-4d56-42ac-bf7d-9477bcf4d10e",
      "driver_phone": "+15551234567",
      "driver_email": "+15551234567@driver.parkulo.com",
      "plate_number": "ABC1234",
      "plate_state": "TX",
      "car_name": "Tesla Model 3",
      "start_time": "2026-10-04T12:00:00-05:00",
      "end_time": "2026-10-04T16:00:00-05:00",
      "hours": 4.0,
      "total_charge": 11.00,
      "platform_fee": 0.75,
      "payment_status": "paid",
      "extension_count": 1,
      "is_active": true,
      "remaining_seconds": 7200
    }
  ]
}

Facility Session Activity

Convenience endpoint for cameras and integrations operating exclusively at a single parking location.

GET /api/v1/owner/lots/<lot_id>/sessions

Returns sessions scoped exclusively to the provided property UUID or 6-digit text code. Takes the same status and limit filters, and the response adds lot_id and lot_name next to status_filter, count and sessions.

REQUEST
curl -X GET "https://parkulo.com/api/v1/owner/lots/9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d/sessions?status=active" \
  -H "Authorization: Bearer <YOUR_OWNER_API_KEY>"
import requests

url = "https://parkulo.com/api/v1/owner/lots/9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d/sessions"
headers = {"Authorization": "Bearer <YOUR_OWNER_API_KEY>"}
params = {"status": "active"}
response = requests.get(url, headers=headers, params=params)
print(response.json())
const response = await fetch("https://parkulo.com/api/v1/owner/lots/9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d/sessions?status=active", {
  headers: { "Authorization": "Bearer <YOUR_OWNER_API_KEY>" }
});
const data = await response.json();
console.log(data);

Automated Plate Validation (LPR)

Camera Sweep Engine

Enables camera scanning rigs and automated enforcement vehicles to verify whether a license plate is currently cleared to park. Automatically checks both hourly parking sessions and active monthly tenant permits.

POST GET /api/v1/owner/validate-plate

Accepts parameters in either JSON payload (POST) or query strings (GET). The same check also works at /api/v1/owner/lots/<lot_id>/validate-plate.

Field Type Required Description
plate_number String Yes License plate string (e.g. ABC1234). Hyphens, slashes and spaces are removed automatically. Missing returns 400.
plate_state String No Two-letter state abbreviation (e.g. TX). Recommended. Without it, the plate alone is matched against sessions, monthly tenants and earlier tickets. It only matters when the same plate is on file under more than one state; then it picks the right car.
lot_id String No Specific facility UUID or 6-digit code. If omitted, checks all properties you own.
REQUEST
curl -X POST https://parkulo.com/api/v1/owner/validate-plate \
  -H "Authorization: Bearer <YOUR_OWNER_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "plate_number": "ABC1234",
    "plate_state": "TX"
  }'
import requests

url = "https://parkulo.com/api/v1/owner/validate-plate"
headers = {
    "Authorization": "Bearer <YOUR_OWNER_API_KEY>",
    "Content-Type": "application/json"
}
payload = {
    "plate_number": "ABC1234",
    "plate_state": "TX"
}
response = requests.post(url, headers=headers, json=payload)
print(response.json())
const response = await fetch("https://parkulo.com/api/v1/owner/validate-plate", {
  method: "POST",
  headers: {
    "Authorization": "Bearer <YOUR_OWNER_API_KEY>",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    plate_number: "ABC1234",
    plate_state: "TX"
  })
});
const data = await response.json();
console.log(data);
EXPECTED RESPONSE 200 OK
{
  "plate_number": "ABC1234",
  "plate_state": "TX",
  "is_valid": true,
  "status": "active",
  "lot_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
  "lot_name": "Downtown Deck",
  "active_session": {
    "session_id": "31bdf721-a010-441f-82bb-70e17d23a7bb",
    "plate_number": "ABC1234",
    "plate_state": "TX",
    "car_name": "Tesla Model 3",
    "start_time": "2026-10-04T12:00:00-05:00",
    "end_time": "2026-10-04T16:00:00-05:00",
    "hours": 4.0,
    "remaining_minutes": 120
  },
  "active_tenant": null,
  "results": [
    {
      "lot_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
      "lot_name": "Downtown Deck",
      "is_valid": true,
      "status": "active",
      "active_session": { "...": "same as above" },
      "active_tenant": null
    }
  ]
}

results has one entry per lot checked, and top-level is_valid is true if the plate is cleared at any of them. status, lot_id, lot_name, active_session and active_tenant appear at the top level only when exactly one lot was checked, so with several lots, read them from results.

Plate Status Codes & Citation Flags

When validating plates via LPR software, inspect the status property to determine appropriate action. Each lot returns the first status that applies, in this order:

Status Value is_valid Meaning & Camera Action
active_tenant true Vehicle is an authorized monthly parking subscriber. Do not ticket.
already_ticketed false Vehicle was already ticketed at this lot today, even if it paid afterwards. Do not ticket again.
active true Vehicle has a paid session or event pass covering right now. Do not ticket.
expired_today false Paid earlier today but paid time has expired. Eligible for citation.
no_session false Vehicle has no record of payment or permit today. Eligible for citation.