Full REST API reference for ParkHub PHP (Laravel 13 + Sanctum).
All endpoints are available under the /api/v1/ prefix.
A legacy /api/ prefix is also supported for backwards compatibility.
- Authentication
- Response Format
- Error Codes
- Rate Limits
- Auth Endpoints
- Setup
- Users & Profile
- GDPR Endpoints
- Parking Lots
- Zones
- Slots
- Bookings
- Recurring Bookings
- Absences
- Vehicles
- Team
- Waitlist
- User Preferences & Stats
- Notifications
- Calendar / iCal
- Webhooks
- Push Notifications
- Modules
- Admin Endpoints
- Public Endpoints
- System & Health
ParkHub uses Laravel Sanctum opaque tokens. API clients can send them as a
Bearer token; the shared browser frontend also persists the active session in
an httpOnly parkhub_token cookie.
After login, include the token in every protected request:
Authorization: Bearer <access_token>
Tokens expire after 7 days. Refresh with POST /api/v1/auth/refresh.
# Save token to a shell variable (username field also accepts an email address)
TOKEN=$(curl -s -X POST https://parking.example.com/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"secret"}' \
| jq -r '.data.tokens.access_token')Most API responses use the standard ParkHub JSON envelope:
{
"success": true,
"data": { "...": "..." },
"error": null,
"meta": null
}Collection responses use the same envelope with an array in data.
Error responses also use the shared envelope:
{
"success": false,
"data": null,
"error": {
"code": "INVALID_CREDENTIALS",
"message": "Invalid username or password"
},
"meta": null
}Some framework-native validation failures may still surface Laravel's default 422 shape while older controllers are being normalized:
{
"message": "The given data was invalid.",
"errors": {
"email": ["The email field is required."]
}
}| Code | HTTP | Meaning |
|---|---|---|
INVALID_CREDENTIALS |
401 | Wrong username or password |
ACCOUNT_DISABLED |
403 | Account deactivated by an admin |
INVALID_PASSWORD |
400/403 | Password confirmation failed |
NO_SLOTS_AVAILABLE |
409 | No free slots in the requested lot/time window |
SLOT_UNAVAILABLE |
409 | Specific slot already booked for the requested time |
INVALID_IMAGE |
422 | Uploaded file is not a valid image |
| Endpoint | Limit | Window |
|---|---|---|
POST /api/v1/auth/login |
10 requests | 1 minute per IP |
POST /api/v1/auth/register |
10 requests | 1 minute per IP |
POST /api/v1/auth/forgot-password |
5 requests | 15 minutes per IP |
Authenticate and receive a Bearer token.
curl -s -X POST https://parking.example.com/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"secret"}'Request:
{
"username": "john.doe",
"password": "secret123"
}The username field accepts either a username or an email address.
Response (HTTP 200):
{
"user": {
"id": "uuid",
"username": "john.doe",
"email": "john@example.com",
"name": "John Doe",
"role": "user",
"is_active": true,
"department": "IT",
"last_login": "2026-02-27T10:00:00.000000Z"
},
"tokens": {
"access_token": "1|abc123...",
"token_type": "Bearer",
"expires_at": "2026-03-06T10:00:00.000000Z"
}
}Register a new user account. Sends a welcome email (queued). Rate limited: 10/min per IP.
curl -s -X POST https://parking.example.com/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{"username":"jane.doe","email":"jane@example.com","password":"securepass123","name":"Jane Doe"}'Request:
{
"username": "jane.doe",
"email": "jane@example.com",
"password": "securepass123",
"name": "Jane Doe"
}Response (HTTP 201): same structure as login.
Rotate the Bearer token. Revokes all existing tokens and issues a new one. Auth required.
curl -s -X POST https://parking.example.com/api/v1/auth/refresh \
-H "Authorization: Bearer $TOKEN"Response:
{
"tokens": {
"access_token": "2|xyz...",
"token_type": "Bearer",
"expires_at": "2026-03-13T10:00:00.000000Z"
}
}Request a password reset link. Rate limited: 5/15min per IP. Returns a generic success message regardless of whether the email exists (prevents user enumeration).
curl -s -X POST https://parking.example.com/api/v1/auth/forgot-password \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com"}'Response: { "message": "If an account with that email exists, a reset link has been sent." }
Check whether initial setup has been completed. No auth required.
curl -s https://parking.example.com/api/v1/setup/status
# {"setup_completed":true}Initialize the application on first run.
Change the default admin password during the setup wizard flow.
Request: { "current_password": "admin", "new_password": "new-strong-password" }
Mark setup as complete and save company name and use case.
Request: { "company_name": "Muster GmbH", "use_case": "company" }
Return the authenticated user's profile.
curl -s https://parking.example.com/api/v1/users/me \
-H "Authorization: Bearer $TOKEN"Update the authenticated user's profile. All fields optional.
curl -s -X PUT https://parking.example.com/api/v1/users/me \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Jane Smith","email":"jane.smith@example.com","phone":"+49 151 12345678"}'Change password. Requires current password. Rotates all tokens.
curl -s -X PATCH https://parking.example.com/api/v1/users/me/password \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"current_password":"old","new_password":"new-min-8-chars"}'Hard-delete the account and all associated data (CASCADE). Requires password confirmation.
curl -s -X DELETE https://parking.example.com/api/v1/users/me/delete \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"password":"current-password"}'Download all personal data as a JSON file. Implements GDPR Art. 20 (Data Portability).
The legacy path /api/v1/user/export is also supported for backwards compatibility.
curl -s https://parking.example.com/api/v1/users/me/export \
-H "Authorization: Bearer $TOKEN" \
-o my-parkhub-data.jsonThe export includes: profile, all bookings, all absences, all vehicles, preferences.
Anonymize the account per GDPR Art. 17 (Right to Erasure). Requires password confirmation.
curl -s -X POST https://parking.example.com/api/v1/users/me/anonymize \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"password":"current-password","reason":"User request"}'What this endpoint does:
- Replaces
name,email,username,phone,picture,departmentwith placeholders - Replaces
vehicle_plateon all bookings with[GELÖSCHT](§147 AO retention compliance) - Deletes absences, vehicles, favourites, notifications, push subscriptions
- Sets
is_active = false - Sets password to an unguessable random string (account becomes permanently inaccessible)
- Revokes all tokens
- Writes a
gdpr_erasureentry to the audit log
Response: { "message": "Account anonymized. Personal data erased per GDPR Art. 17." }
All lot endpoints require authentication.
List all parking lots with real-time available slot counts.
curl -s https://parking.example.com/api/v1/lots \
-H "Authorization: Bearer $TOKEN"Response:
[
{
"id": "uuid",
"name": "P+R Hauptbahnhof",
"address": "Bahnhofplatz 1, 80335 München",
"total_slots": 50,
"available_slots": 23,
"status": "open",
"layout": null
}
]Create a parking lot. Admin required.
curl -s -X POST https://parking.example.com/api/v1/lots \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Tiefgarage Ost","address":"Musterstraße 1, 10115 Berlin","total_slots":40,"status":"open"}'Get a lot including layout. Auto-generates layout from slots if none saved.
Update lot name, address, total_slots, layout JSON, or status.
Delete a lot and all its slots (CASCADE).
List all slots for a lot including current booking status.
Real-time occupancy figures.
curl -s "https://parking.example.com/api/v1/lots/LOT_UUID/occupancy" \
-H "Authorization: Bearer $TOKEN"Response:
{
"lot_id": "uuid",
"lot_name": "P+R Hauptbahnhof",
"total": 50,
"occupied": 27,
"available": 23,
"percentage": 54
}Generate a QR code URL for quick booking at this lot.
Generate a QR code URL for a specific slot.
List zones for a lot.
Create a zone.
{ "name": "UG1", "color": "#3b82f6", "description": "Untergeschoss 1" }Update a zone.
Delete a zone (sets zone_id to null on slots in this zone).
Create a slot in a lot.
curl -s -X POST "https://parking.example.com/api/v1/lots/LOT_UUID/slots" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"slot_number":"A-01","status":"available","zone_id":"ZONE_UUID","reserved_for_department":"Management"}'Update slot number, status, zone, or department reservation.
Delete a slot.
List the authenticated user's bookings. Optional filters:
?status=confirmed— filter by status?from_date=2026-01-01— bookings starting on or after this date?to_date=2026-12-31— bookings ending on or before this date
curl -s "https://parking.example.com/api/v1/bookings?status=confirmed" \
-H "Authorization: Bearer $TOKEN"Create a booking. If slot_id is omitted, the system auto-assigns the first available slot.
Sends a booking confirmation email (queued).
curl -s -X POST https://parking.example.com/api/v1/bookings \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"lot_id": "LOT_UUID",
"slot_id": "SLOT_UUID",
"start_time": "2026-03-01T08:00:00",
"end_time": "2026-03-01T18:00:00",
"booking_type": "einmalig",
"vehicle_plate": "M-AB 1234",
"notes": "Near entrance please"
}'Returns HTTP 409 if slot is unavailable or no slots available in the lot.
One-tap booking. Auto-assigns the best available slot in a lot.
Request: { "lot_id": "uuid", "start_time": "...", "end_time": "..." }
Return the authenticated user's CO2 impact summary for dashboard parity with the Rust backend.
Optional query parameters:
?from=2026-03-01— include bookings starting on or after this date?to=2026-03-31— include bookings up to this date?lot_id=LOT_UUID— limit the summary to one parking lot
Response data includes bookings_counted, total_km, emitted_g,
counterfactual_g, saved_g, carpool_saved_g, and rounded saved_kg.
Update booking fields (vehicle_plate, notes).
Cancel a booking.
Create a booking for a named guest (no user account needed for the guest).
curl -s -X POST https://parking.example.com/api/v1/bookings/guest \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"lot_id": "LOT_UUID",
"slot_id": "SLOT_UUID",
"guest_name": "Max Mustermann",
"start_time": "2026-03-01T09:00:00",
"end_time": "2026-03-01T12:00:00",
"vehicle_plate": "B-CD 5678"
}'Swap two bookings between users.
Create a swap request for a booking.
Accept or reject a swap request.
Request: { "action": "accept" } or { "action": "reject" }
Mark a booking as checked in (validates QR code scan).
Update notes on a booking.
Get an HTML invoice (printer-friendly, use browser Print → Save as PDF).
curl -s "https://parking.example.com/api/v1/bookings/BOOKING_UUID/invoice" \
-H "Authorization: Bearer $TOKEN"Return bookings formatted as calendar events.
List recurring booking patterns for the authenticated user.
Create a recurring booking pattern.
curl -s -X POST https://parking.example.com/api/v1/recurring-bookings \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"lot_id": "LOT_UUID",
"slot_id": "SLOT_UUID",
"days_of_week": [0,1,2,3,4],
"start_date": "2026-03-01",
"end_date": "2026-12-31",
"start_time": "08:00",
"end_time": "18:00",
"vehicle_plate": "M-AB 1234"
}'days_of_week: 0 = Monday, 1 = Tuesday, ..., 6 = Sunday.
Update a recurring pattern.
Deactivate or delete a recurring pattern.
List absences for the authenticated user.
Create an absence.
curl -s -X POST https://parking.example.com/api/v1/absences \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"absence_type":"homeoffice","start_date":"2026-03-10","end_date":"2026-03-10","note":"Working from home"}'absence_type: homeoffice, vacation, sick, training, other
Update an absence.
Delete an absence.
Get the user's recurring absence pattern (e.g. every Monday homeoffice).
Set the recurring absence pattern.
Request: { "weekdays": [0] } (array of day indices: 0=Monday)
View all users' absences for team planning.
Import absences from an iCal .ics file.
Request: multipart/form-data with ical_file field.
List the authenticated user's vehicles.
Add a vehicle.
curl -s -X POST https://parking.example.com/api/v1/vehicles \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"plate":"M-AB 1234","make":"BMW","model":"3 Series","color":"Blau","is_default":true}'Update a vehicle.
Delete a vehicle and its stored photo.
Upload a vehicle photo.
curl -s -X POST "https://parking.example.com/api/v1/vehicles/VEHICLE_UUID/photo" \
-H "Authorization: Bearer $TOKEN" \
-F "photo=@/path/to/photo.jpg"Accepts:
multipart/form-datawithphotofield (JPEG/PNG/GIF/WebP, max 5 MB)- JSON with
photo_base64(base64-encoded image, max 8 MB)
Images are validated through PHP GD and resized to max 800px on the longer edge.
Serve the stored vehicle photo as image/jpeg.
Return a list of 400+ German Kfz-Unterscheidungszeichen (licence plate city codes) with city names.
curl -s https://parking.example.com/api/v1/vehicles/city-codes \
-H "Authorization: Bearer $TOKEN"List all active users (for team visibility features).
Show who is in the office, on homeoffice, or on vacation today.
List the user's waitlist entries.
Join the waitlist for a lot on a specific date.
Request: { "lot_id": "uuid", "requested_date": "2026-03-15" }
Leave the waitlist.
Return user preferences.
Update preferences.
curl -s -X PUT https://parking.example.com/api/v1/user/preferences \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"language": "de",
"theme": "dark",
"notifications_enabled": true,
"email_notifications": true,
"push": false,
"show_plate_in_calendar": true,
"default_lot_id": "LOT_UUID",
"locale": "de-DE",
"timezone": "Europe/Berlin"
}'push is the canonical public preference key. Compatibility clients may still send
push_notifications, but responses normalize back to push.
Return statistics: total bookings, bookings this month, homeoffice days, favourite slot.
List the user's favourite slots.
Add a slot to favourites. Request: { "slot_id": "uuid" }
Remove a slot from favourites.
List the last 50 in-app notifications for the user.
Mark a notification as read.
Mark all notifications as read.
Download the user's bookings as an iCal .ics file.
Import into any calendar application (Google Calendar, Apple Calendar, Outlook).
curl -s "https://parking.example.com/api/v1/user/calendar.ics" \
-H "Authorization: Bearer $TOKEN" \
-o my-bookings.icsList the user's registered webhooks.
Create a webhook.
curl -s -X POST https://parking.example.com/api/v1/webhooks \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.example.com/parkhub",
"events": ["booking.created","booking.cancelled"],
"secret": "optional-hmac-secret"
}'Update a webhook.
Delete a webhook.
Register a Web Push subscription.
Remove all push subscriptions for the authenticated user.
Current in v5.0.1.
The Modular UX platform exposes the full installed module registry over REST. See FEATURES.md § Modular UX Platform for the product overview, and the OpenAPI snapshot at docs/openapi/php.json for the canonical schema.
Read endpoints are open to any authenticated user. Write endpoints under /api/v1/admin/modules/* require role=admin or role=superadmin — authz is layered: the admin middleware runs first, then Laravel Policies take over per-action (see app/Policies/*). Request bodies are validated via FormRequest classes; JSON Schema validation for config writes uses opis/json-schema 2.6.0.
List every installed module with enriched metadata. The response envelope keeps the legacy flat modules map in place so older clients still work.
Public module slugs use the canonical product names. For example:
realtimeis the public capability namebroadcastingandwebsocketare accepted as legacy aliasespushis the public capability namepush_notificationsandweb_pushstay internal / legacy wiring names
curl -s https://parking.example.com/api/v1/modules \
-H "Authorization: Bearer $TOKEN"Response:
{
"modules": { "bookings": true, "stripe": false, "announcements": true },
"module_info": [
{
"name": "announcements",
"category": "Notification",
"description": "Admin-authored announcements shown to users on login.",
"enabled": true,
"runtime_toggleable": true,
"runtime_enabled": true,
"config_keys": ["announcement.max_active"],
"depends_on": [],
"ui_route": "/announcements",
"version": "5.0.1",
"config_schema": { "type": "object", "properties": { "...": {} } }
}
],
"version": "5.0.1"
}runtime_enabled reflects the effective state after applying any admin override. For rows with runtime_toggleable = false, it always equals enabled.
Compatibility alias lookups on GET /api/v1/modules/{name} remain supported: /modules/broadcasting and /modules/websocket both resolve to the canonical realtime module, while /modules/push_notifications and /modules/web_push resolve to push.
Return a single ModuleInfo. Returns 404 if the slug is unknown.
curl -s https://parking.example.com/api/v1/modules/announcements \
-H "Authorization: Bearer $TOKEN"Flip a module's runtime_enabled bit without redeploying. Admin-only. Body is bound to UpdateModuleRequest (FormRequest).
curl -s -X PATCH https://parking.example.com/api/v1/admin/modules/announcements \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"runtime_enabled": false}'Response: the updated ModuleInfo (same shape as GET /api/v1/modules/{name}).
Error codes:
| Code | HTTP | When |
|---|---|---|
| — | 404 | Unknown module slug |
| — | 409 | Module has runtime_toggleable = false (security-sensitive row) |
| — | 403 | Caller is not an admin |
| — | 422 | FormRequest validation rejected the body shape |
Every successful toggle writes an AuditLog row with action = 'module_config_updated'.
Return the module's JSON Schema plus the currently-persisted values for each schema property. Admin-only.
curl -s https://parking.example.com/api/v1/admin/modules/themes/config \
-H "Authorization: Bearer $TOKEN"Response:
{
"schema": {
"type": "object",
"properties": {
"default_theme": { "type": "string", "enum": ["light", "dark", "auto"] }
},
"required": ["default_theme"]
},
"values": { "default_theme": "dark" }
}Error codes: 404 for unknown slug, 400 for a module without a declared config_schema.
Persist new values for the module's config. Admin-only. Body is bound to UpdateModuleConfigRequest, then validated against the module's JSON Schema via opis/json-schema 2.6.0. Failures return 422 with the structured error envelope below.
curl -s -X PATCH https://parking.example.com/api/v1/modules/themes/config \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"values": {"default_theme": "light"}}'Success response: the updated {schema, values} pair (same shape as the GET).
Validation failure response (422):
{
"success": false,
"data": null,
"error": {
"code": "CONFIG_VALIDATION_FAILED",
"message": "Config payload failed validation.",
"details": {
"values.default_theme": [
"The selected values.default_theme is invalid."
]
}
},
"meta": null
}Other error codes:
| Code | HTTP | When |
|---|---|---|
| — | 404 | Unknown module slug |
| — | 400 | Module has no declared config_schema |
| — | 403 | Caller is not an admin |
Every successful write emits an AuditLog row with action = 'module_config_updated'.
All /admin/* endpoints require role=admin or role=superadmin.
Dashboard statistics: total users, lots, slots, bookings, active bookings, occupancy %, homeoffice count today.
curl -s https://parking.example.com/api/v1/admin/stats \
-H "Authorization: Bearer $TOKEN"Booking heatmap by day of week and hour. Query: ?days=30
Booking reports for a period. Query: ?days=30
Chart data: booking trend + current occupancy. Query: ?days=7
Paginated audit log. Supports: ?action=login_failed, ?search=username, ?per_page=50
# Create announcement
curl -s -X POST https://parking.example.com/api/v1/admin/announcements \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"title":"Maintenance","message":"Lot B closed Friday.","severity":"warning"}'severity: info, warning, error, success
GET /api/v1/admin/announcements— list allPOST /api/v1/admin/announcements— createPUT /api/v1/admin/announcements/:id— updateDELETE /api/v1/admin/announcements/:id— delete
GET /api/v1/admin/users— list all usersPUT /api/v1/admin/users/:id— update (name, email, role, is_active, department, password)DELETE /api/v1/admin/users/:id— delete user (cannot delete your own account)
curl -s -X POST https://parking.example.com/api/v1/admin/users/import \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"users": [
{"username":"max.mueller","email":"max@example.com","name":"Max Müller","role":"user","department":"IT","password":"initial-pass"}
]
}'Up to 500 users per request. Skips existing usernames and emails.
curl -s "https://parking.example.com/api/v1/admin/bookings/export" \
-H "Authorization: Bearer $TOKEN" \
-o bookings.csvGET /api/v1/admin/settings— return all settingsPUT /api/v1/admin/settings— update:company_name,use_case,self_registration,license_plate_mode,max_bookings_per_day,allow_guest_bookings,auto_release_minutes,require_vehicle,primary_color,secondary_color
GET /api/v1/admin/settings/emailPUT /api/v1/admin/settings/email— fields:smtp_host,smtp_port,smtp_user,smtp_pass,from_email,from_name,enabled
GET /api/v1/admin/settings/auto-releasePUT /api/v1/admin/settings/auto-release— body:{ "enabled": true, "timeout_minutes": 30 }
GET /api/v1/admin/settings/webhooksPUT /api/v1/admin/settings/webhooks
GET /api/v1/admin/brandingPUT /api/v1/admin/branding— fields:company_name,primary_color,logo_urlPOST /api/v1/admin/branding/logo— upload logo (JPEG/PNG, max 2 MB, multipart field:logo)
GET /api/v1/admin/privacyPUT /api/v1/admin/privacy— fields:policy_text,data_retention_days,gdpr_enabled
GET /api/v1/admin/impressum— return current Impressum for editingPUT /api/v1/admin/impressum— update:provider_name,provider_legal_form,street,zip_city,country,email,phone,register_court,register_number,vat_id,responsible_person,custom_text
PATCH /api/v1/admin/slots/:id— update slot number, status, zone, departmentDELETE /api/v1/admin/lots/:id— delete lot and all slots
curl -s -X POST https://parking.example.com/api/v1/admin/reset \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"confirm":"RESET"}'Deletes all user data (bookings, absences, vehicles) except the calling admin's account.
Requires { "confirm": "RESET" } to prevent accidental invocation.
Real-time occupancy for all lots. Suitable for embedding in lobby displays.
Extended occupancy display data.
Return currently active, non-expired announcements.
Return public branding settings (company name, primary color, logo URL).
Serve the branding logo. Returns a default SVG "P" icon if no logo has been uploaded.
Return the Impressum (DDG §5) fields. Must be publicly accessible per German law.
curl -s https://parking.example.com/api/v1/legal/impressumLiveness probe — returns HTTP 200 if the PHP process is running.
curl https://parking.example.com/api/v1/health/live
# {"status":"ok"}Readiness probe — returns HTTP 200 if the database is reachable.
Return the current application version.
Return maintenance mode status.