Developer API Reference
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.
Authorization: Bearer <YOUR_DRIVER_TOKEN>
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.
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);
{
"status": "code_sent",
"user_id": "94b81ef0-4d56-42ac-bf7d-9477bcf4d10e"
}
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.
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);
{
"status": "authenticated",
"token": "pt_Xq3vN8kL2mR7tY1wZ5cB9dF4gH6jK0pS",
"token_type": "Bearer"
}
Vehicle Registry
Manage vehicles and license plates associated with the authenticated driver account.
Retrieves all vehicles registered to your account.
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);
{
"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
}
]
}
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.
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);
{
"status": "created",
"vehicle_id": "4c8310ba-d52f-4b1e-8a7c-61e9f03d2c58"
}
Removes one of your vehicles. A vehicle that isn't on your account returns 404.
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);
{
"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.
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.
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);
{
"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
}
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.
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);
{
"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.
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. |
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);
{
"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.
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.
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);
{
"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
}
Ends the session now, for example once departure sensors trigger. Ending early doesn't refund unused time.
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);
{
"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.
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.
curl -X GET https://parkulo.com/api/v1/sessions/active \ -H "Authorization: Bearer <YOUR_DRIVER_TOKEN>"
{
"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.
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."} |
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.
Authentication Headers
Pass your provisioned API key in each request using either standard Bearer token authorization or the X-API-Key header:
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.
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.
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);
{
"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"
}
]
}
Returns the same lot fields as above plus usage analytics: the busiest days, the busiest start hours, and the most frequent vehicles.
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);
{
"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.
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). |
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);
{
"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.
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.
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 EngineEnables 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.
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. |
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);
{
"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. |