Contents

REST API - Plugixa Car Parking

Everything the admin app, the booking form and the account page do, they do over REST, under the namespace plugixa-car-parking/v1. Every rule is enforced at the route, not in the page.

This is the API the plugin’s own screens use. It is not a separately versioned public API, and the plugin makes no promise that routes, parameters or response shapes stay the same between versions. Pin the plugin version you build against, and test after updating.

Authentication

There are two kinds of route.

Admin routes are for staff. The plugin adds no authentication of its own: it relies on whatever WordPress provides, then checks the capabilities of the user WordPress identified.

Caller How
The admin app The logged-in cookie plus a REST nonce in the X-WP-Nonce header, the normal WordPress way
A script or another system Any method WordPress accepts for REST requests, such as an application password for an account that holds the capabilities below

Public routes are open to visitors, because the booking form and the account page run for people who are not signed in. They are protected differently. See Public routes.

Capabilities

The plugin defines six capabilities a role can be given, and one it works out by itself.

Capability What it allows
plugixa_carparking_view Read records: every list, detail screen, the dashboard and the calendar
plugixa_carparking_create Create records, and record a payment
plugixa_carparking_edit Change records, refund a payment, run a mass update
plugixa_carparking_delete Move records to the trash, restore them, delete them permanently, run a mass delete
plugixa_carparking_export Download the bookings CSV
plugixa_carparking_manage_settings Read and save the plugin’s settings
plugixa_carparking_manage Open the admin app. Never granted directly: anyone who holds one of the six above has it.

Activation grants all of them to the Administrator role and to nobody else. A user with the WordPress manage_options capability is always treated as holding every one. The plugin has no screen for assigning them to other roles. They are ordinary WordPress capabilities, so any role editor plugin can grant them.

Two settings can be changed only by a user who also has manage_options: Erase all data when the plugin is deleted (delete_data_on_uninstall) and custom_css. A settings manager without it can save everything else.

A caller who fails the check gets the code plugixa_carparking_forbidden and the message “You are not allowed to do that.”, with status 401 when not signed in and 403 when signed in. The verdict of every check passes through the plugixa_carparking_user_can filter. See Hooks reference.

Request and response shape

Send request bodies as JSON with Content-Type: application/json.

A successful admin response wraps its payload. A list adds meta:

{
  "success": true,
  "data": [],
  "meta": { "total": 42, "pages": 1 }
}

A refusal is a standard WordPress REST error with a code, a readable message and an HTTP status.

Code Status When
plugixa_carparking_forbidden 401 or 403 The capability check failed
plugixa_carparking_conflict 409 Somebody else changed the record while you were editing it
plugixa_carparking_<entity>_in_use 409 A listener on one of the ..._delete_blockers filters refused the trash. Nothing in version 1.0.0 attaches one.
plugixa_carparking_rate_limited 429 A public route’s rate limit was reached. The response carries a Retry-After header.
plugixa_carparking_rejected 400 The honeypot field of the booking route was filled in
plugixa_carparking_captcha_failed 403 A CAPTCHA attached through the filter refused the request
rest_error 403, 404, 409 or 422 A public route refused the request. message is a sentence for the customer.

Edit conflicts

Every record carries modified_at. When an update sends back the modified_at it read, the plugin compares it with the stored one and answers 409 plugixa_carparking_conflict if they differ: “Somebody else changed this record while you were editing it. Reload to see their version before saving yours.”

The check is opt-in. An update that sends no modified_at is not checked. Two saves inside the same second cannot be told apart.

List parameters

Every list route accepts the same parameters.

Parameter Default Notes
page 1
per_page 50 From 1 to 200
search Matches the entity’s own search columns
orderby id A column the entity allows sorting by
order desc asc or desc
view trash lists trashed records only

Filters are sent as query parameters too. GET /filters/<entity_type> returns what a list of that entity may be filtered by.

Record routes

Each entity below has the same set of routes. <base> is its path.

Method Route Needs What it does
GET /<base> view List
POST /<base> create Create
GET /<base>/<id> view Read one
PUT, PATCH /<base>/<id> edit Update
DELETE /<base>/<id> delete Move to the trash
POST /<base>/<id>/restore delete Bring back from the trash
DELETE /<base>/<id>/purge delete Delete permanently
Entity <base>
Bookings bookings
Locations locations
Spaces at a location location-spaces
Space types space-types
Coupons coupons
Booking forms booking-forms
Extras PRO booking-extras
Tax rates PRO tax-rates
Pricing rules PRO pricing-rules
Customer groups PRO customer-groups

The Pro routes exist only when Pro is installed. On a free site they answer with WordPress’s ordinary 404 for an unknown route.

Other admin routes

Method Route Needs What it does
GET /bootstrap view What the admin app needs to start: entities, the caller’s permissions, the current user and the display settings
GET /meta/entities view The registered entities
GET /users?search= view Users who may open the app, by display name, for pickers
GET /filters/<entity_type> view on that entity The filters a list of that entity accepts
POST /mass-action/<entity_type> edit, or delete for a delete Apply one action to many records
GET /settings manage settings The settings, with every secret blanked, and the choices each control offers
POST, PUT /settings manage settings Save settings. A partial body is a partial save.
GET /dashboard view on bookings and payments The dashboard figures
GET /calendar view Bookings between from and to, optionally by location_id, space_type_id and status
GET /bookings/export export The bookings CSV, following the same search and filters as the list. At most 10,000 rows.
POST /bookings/quote view Price a selection for the admin booking form
GET, PUT, POST /locations/<id>/hours view to read, edit to write A location’s opening hours
GET, POST /locations/<id>/exceptions view to read, edit to write A location’s date exceptions
PUT, PATCH, DELETE /locations/<id>/exceptions/<exception_id> edit Change or remove one exception
GET, PUT /space-types/<id>/prices view to read, edit to write A space type’s prices
GET /payments view on payments and bookings The payments ledger. booking_id narrows it to one booking.
POST /payments create Record a payment against a booking
GET /payments/<id> view on payments and bookings One payment
POST /payments/<id>/refund edit Refund a payment
GET /notification-log view The message log. booking_id narrows it to one booking.
GET, POST /customer-groups/<id>/members PRO view to read, edit to write The members of a customer group
DELETE /customer-groups/<id>/members/<user_id> PRO edit Remove a member

Mass actions

POST /mass-action/<entity_type> takes this body:

{
  "action": "update",
  "ids": [12, 13, 14],
  "values": { "status": "confirmed" }
}

action is update or delete. A delete moves the records to the trash. One request acts on at most 200 records. The permission is asked again for every record: one the caller may not touch is skipped and counted, and the rest are still processed. The answer reports done, skipped and failed.

Saving settings

A secret, such as an API key, is never sent to the browser. GET /settings returns it as an empty string, with a second key ending in _set that says whether one is stored. Sending a secret back empty leaves it unchanged. To erase a stored secret, name its key in a clear list:

{ "clear": ["stripe_secret_key"] }

A key the settings schema does not list is dropped from a save.

Public routes

Method Route Rate limit bucket What it does
GET, POST /availability availability What is free at a location between entry and exit, with a price for one space of each type. Needs location_id, entry and exit. space_type_id is optional.
POST /quote quote, and quote_coupon when a coupon code is sent The full price of a selection
POST /booking booking Create a booking
POST /coupon/validate coupon_validate Check a coupon code
GET /account/bookings account_list signed in, account_lookup as a guest A signed-in customer’s bookings, or one booking found by ref and email
POST /account/bookings/<ref>/cancel account_change Cancel a booking
POST /account/bookings/<ref>/reschedule PRO account_change Move a booking to new dates
POST /payment/start payment_start Start a payment for a booking. Takes booking_ref, gateway and optionally pay_type.
GET, POST /payment/return Where a payment provider sends the customer’s browser back to
POST /payment/webhook/<gateway> Where a payment provider reports a payment
GET /invoice PRO invoice The PDF invoice of a booking. Takes ref and token.

A public success always carries a message:

{
  "success": true,
  "message": "Your booking has been created.",
  "data": { }
}

How public routes are protected

A public route cannot ask who is calling, so it checks three things, cheapest first.

  1. Honeypot. The booking route refuses a request whose hidden pcp_hp field is filled in, with “This request could not be processed.”
  2. Rate limit. Counted per visitor address and per bucket.
  3. CAPTCHA. Nothing is bundled. A site adds one with the plugixa_carparking_public_request_captcha filter.

The routes that change something (/booking, cancel, reschedule) also need the public nonce the plugin’s pages print. Send it as security in the body or in the X-Plugixa-Carparking-Nonce header. Without a valid one the answer is 403: “Your session expired. Please refresh and try again.” The nonce shows that the request came from one of the site’s own pages. It identifies nobody.

A booking created through /booking belongs to whoever is signed in. A user_id in the body is ignored.

Rate limits

Bucket Requests Window
availability 120 10 minutes
quote 240 10 minutes
quote_coupon 30 10 minutes
booking 10 1 hour
payment_start 20 1 hour
account_lookup 10 15 minutes
account_list 60 10 minutes
account_change 10 1 hour
coupon_validate 10 10 minutes
invoice 30 10 minutes

Over the limit the answer is 429 “Too many requests. Please wait a little while and try again.”, with a Retry-After header in seconds.

The counter is kept in a transient whose name contains a hash of the address, never the address itself. The address is read from REMOTE_ADDR only. On a site behind a proxy every visitor shares the proxy’s address and therefore one counter: use the plugixa_carparking_client_ip filter to name the header your proxy sets. The limits themselves are changed with plugixa_carparking_rate_limits. Both are shown in Hooks reference.

The old namespace

An earlier line of the plugin served its routes from plugixacarparking/v1. A few addresses are written down outside the plugin, where an update cannot reach them, so they still answer there as well:

Old address Why it is kept
plugixacarparking/v1/payment/webhook/<gateway> Pasted into a payment provider’s dashboard
plugixacarparking/v1/payment/return Where a provider sends a customer back to
plugixacarparking/v1/invoice PRO Linked from emails customers already hold

Each is the same route under a second name: the same handler and the same checks. Every other address on the old namespace answers 404. A module can keep another path alive with the plugixa_carparking_legacy_route_aliases filter.

Version 1.0.0 itself still writes these three addresses on the old namespace: the Stripe webhook address shown in Settings, the return address given to a payment provider, and the PDF invoice link. Both namespaces reach the same handler, so either works.

Adding your own routes

Register on the plugixa_carparking_register_rest_routes action, which passes the namespace:

add_action( 'plugixa_carparking_register_rest_routes', function ( $namespace ) {
	register_rest_route(
		$namespace,
		'/gate-status',
		array(
			'methods'             => 'GET',
			'callback'            => fn() => rest_ensure_response( array( 'open' => true ) ),
			'permission_callback' => fn() => current_user_can( 'plugixa_carparking_view' ),
		)
	);
} );

Quick Links