Contents
On this page

Webhooks - Plugixa Boat Rental

Webhooks PRO send a message to another system each time something happens to a booking: made, confirmed, moved, cancelled, paid, refunded, checked out or checked in at the dock. Each message is JSON, posted to an address you give, signed with a secret so the receiver can check it came from your site, and retried for a day if it is not accepted.

Use it to connect Zapier, Make, n8n or your own system. It is an app, off by default: switch it on in the App manager. The screen is Plugixa Boat Rental -> Webhooks, in the Configuration menu group.

Adding an endpoint

Press New endpoint.

Field What it does
Name For you, for example “Zapier: new bookings”.
Address (https) The address your receiver gives you. It must start with https://.
Events The events this endpoint wants.
On Switch the endpoint off to stop sending to it.

Press Save. The endpoint gets a signing secret, starting with whsec_. Show secret shows it, to copy into your receiver. New secret replaces it: update your receiver at once, because messages are signed with the new one from then on.

Send test posts a ping event to that endpoint straight away, whether it is switched on or not. A test is never retried and never counts as a failure.

The Webhooks screen with one endpoint, its events, its shortened secret and Send test, Show secret and New secret, and the Delivery log with a test ping marked Failed because the capture site makes no outgoing calls

Which addresses are allowed

The address must be https, with no user name or password in it. Addresses on your own server or private network are refused (localhost, private, loopback and link-local ranges), when you save and again before every delivery.

Events

Event Sent when
booking.created A booking is made, on the site or in the office.
booking.confirmed A booking’s status becomes confirmed.
booking.updated A booking’s dates, boat, unit, marina or guests change, or it is repriced.
booking.cancelled A booking is cancelled, by the customer or the office.
payment.captured A payment is recorded on a booking.
payment.refunded A refund is recorded on a booking.
checkout.completed A boat is checked out in the dock app.
checkin.completed A boat is checked in in the dock app.
request.created A customer’s request to book is waiting for your answer.
request.approved You approve a request.
request.declined You decline a request.
request.closed A request ends another way: nobody answered in time, or it was closed before an answer.
access_code.released A self check-in code is given to the customer.
ping You press Send test.

The list also has access_code.with_code. It is not an event: it is a flag. Tick it with access_code.released and that endpoint’s messages carry the code itself. Without it, they say only that a code was released.

A booking sends each event at most once per change, even when several things happen to it at the same moment.

The message

Every message has the same envelope:

{
  "id": "evt_3f2a9c6e1b7d4e0a8c5b2d1f6e9a7c4b",
  "type": "booking.created",
  "created": 1794300000,
  "api_version": "2026-11",
  "data": {
    "booking": {
      "id": 412,
      "reference": "PBR-7Q4K2",
      "status": "confirmed",
      "location_id": 2,
      "boat_id": 5,
      "unit_id": 9,
      "start": "2026-11-14 10:00:00",
      "end": "2026-11-14 14:00:00",
      "start_utc": "2026-11-14 09:00:00",
      "end_utc": "2026-11-14 13:00:00",
      "guests": 4,
      "quantity": 1,
      "currency": "EUR",
      "total": 380,
      "amount_paid": 0,
      "source": "web",
      "created_at": "2026-11-08 16:20:00",
      "customer": {
        "name": "Ana Torres",
        "email": "ana@example.com",
        "phone": "+34600000000"
      }
    }
  }
}
  • created is a Unix timestamp. start and end are on the marina’s clock; start_utc and end_utc are in UTC.
  • unit_id is null while no unit is assigned.
  • source is where the booking came from: web, admin, walkup or import.
  • customer is left out when Leave the customer’s name, email and phone out of messages is on (below).

Some events add to data:

Event Adds
payment.captured, payment.refunded payment (id, gateway, type, amount, currency, status, created_at) and total_paid
checkout.completed, checkin.completed report (id, flagged, created_at)
request.declined, request.closed state: why the request ended
access_code.released access_code (unit_id, released_at, and code only with the access_code.with_code flag)
ping Only message, no booking

Card details are never sent: the plugin holds none.

Headers

Header Holds
Content-Type application/json
Plugixa-Event The event, for example booking.created
Plugixa-Event-Id The event’s id, the same as id in the body. A retry or a resend carries the same id, so use it to ignore duplicates.
Plugixa-Signature The signature (below)

Checking the signature

The Plugixa-Signature header looks like this:

Plugixa-Signature: t=1794300000,v1=5f2b8c...e91a

t is the Unix time the message was sent. v1 is a hex HMAC-SHA256, keyed with the endpoint’s secret, of the text t, a dot, and the raw body.

To check a message:

  1. Read the raw body before any JSON parsing.
  2. Work out the HMAC-SHA256 of t + "." + raw body with your secret.
  3. Compare it with v1 in constant time.
  4. Refuse a message whose t is more than 5 minutes from now, so a captured message cannot be replayed later.

In PHP:

$secret = 'whsec_your_secret';
$body   = file_get_contents( 'php://input' );
$header = $_SERVER['HTTP_PLUGIXA_SIGNATURE'] ?? '';

preg_match( '/t=(\d+)/', $header, $t );
preg_match_all( '/v1=([0-9a-f]{64})/', $header, $v1 );
$expected = hash_hmac( 'sha256', ( $t[1] ?? '' ) . '.' . $body, $secret );
$valid    = isset( $t[1] ) && abs( time() - (int) $t[1] ) <= 300
	&& array_filter( $v1[1], fn( $sig ) => hash_equals( $expected, $sig ) );

if ( ! $valid ) {
	http_response_code( 400 );
	exit;
}

$event = json_decode( $body, true ); // Ignore an $event['id'] you have already handled.
http_response_code( 200 );

In Node, with Express:

const crypto = require('crypto');

app.post('/hook', express.raw({ type: 'application/json' }), (req, res) => {
  const header = req.get('Plugixa-Signature') || '';
  const t = (header.match(/t=(\d+)/) || [])[1];
  const sigs = [...header.matchAll(/v1=([0-9a-f]{64})/g)].map((m) => m[1]);
  const expected = crypto
    .createHmac('sha256', process.env.PLUGIXA_SECRET)
    .update(`${t}.${req.body}`)
    .digest('hex');
  const fresh = t && Math.abs(Date.now() / 1000 - Number(t)) <= 300;
  const ok = fresh && sigs.some((s) => crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)));

  if (!ok) return res.sendStatus(400);

  const event = JSON.parse(req.body); // Ignore an event.id you have already handled.
  res.sendStatus(200);
});

Delivery and retries

Messages are sent within a minute of the event, by WordPress’s scheduled tasks. On a site with little traffic, a real server cron job keeps them on time.

  • Any 2xx answer counts as delivered. Redirects are not followed. The request times out after 10 seconds.
  • After a failure, the message is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours: 7 tries in all. Then it is marked failed.
  • Each endpoint is tried on its own, so one failing never holds up another.
  • When 20 events in a row could not be delivered to an endpoint, it is switched off and the office is emailed. Switching it back on starts the count again.

The delivery log

Delivery log lists every message with its Event, Endpoint, Status (Delivered, Waiting to retry, Failed or Not tried yet), Tries and When. View shows the Message sent and the first part of the Answer received, with the HTTP code or error. Resend sends it again from the first try.

The log is kept for 30 days.

Settings

Under Plugixa Boat Rental -> Settings -> Webhooks:

Setting Default What it does
Leave the customer’s name, email and phone out of messages Off For receivers that only need the booking’s dates, boat and amounts.

Who can do what

The Webhooks screen has its own permissions: read to see endpoints and the log, create to add an endpoint, edit to change one, show or replace its secret, send a test or resend a message, and delete to remove one. See Roles and staff access.

For pulling data on demand rather than receiving it, see REST API and API keys.

Quick Links

Share