Appointment booking hooks

Available in:Free

These hooks change how HappenBoard offers and records appointments: which times can be booked, who hosts them, what they cost, how they are paid and what happens at each step. Reach for them to block times from another system, add a room picker, price by time of day or sync bookings to a CRM.

If you are new to HappenBoard hooks, start with Extending HappenBoard with hooks. The feature itself is explained in Appointment booking: overview. The emails these actions send have their own notification and email hooks.

Booking statuses and the actions they fire

Each booking is saved with a status, then moves on when it is paid, approved, declined, cancelled or moved. The table lists the actions fired at each step, in order, without their happenboard_booking_ prefix.

What happensStatus savedActions, in order
Booked, nothing to approve or payconfirmedcreated, confirmed
Request on a type that needs approvalpending_approvalcreated, requested
Paid online (Pro)pending_payment, then confirmedcreated, payment_pending, then paid, confirmed
Request with the card held (Pro)pending_payment, then pending_approvalcreated, payment_pending, then payment_authorized, requested
Request approvedconfirmedapproved, confirmed, then payment_captured if a held card was charged
Request declineddeclineddeclined, cancelled
Booking cancelledcancelledcancelled
Checkout never completedcancelledpayment_abandoned
Booking movedunchangedrescheduled
Marked as completed or no-show, once startedcompleted or no_shownone

How offered times are checked

  • The Booking Page asks for one month of times at a time. One request covers at most 186 days and returns at most 750 slots.
  • When a booking is made or moved, HappenBoard runs the engine again for that day, uncached, and refuses a start it no longer offers. So happenboard_booking_slots decides what can be booked, not only what is shown.
  • That check and the write run under one lock for the whole site, so two visitors picking the same time are served one after the other.
  • A visitor who waits more than 10 seconds for the lock is asked to try again (happenboard_booking_busy, HTTP 503). Keep the filters that run during the check fast, without remote requests.
  • A host is busy during every booking they hold, of any booking type, widened by that booking type’s buffers.
  • Seats are shared only by bookings of the same type that start at the same time. Day-based types and types where the customer picks the length share their capacity across overlapping bookings.
  • A booking type’s daily cap hides a host for a day once the bookings they start that day, in the site’s timezone, reach the cap.
  • Slot lists are cached only when the site has a persistent object cache. A browser or CDN may reuse an anonymous visitor’s list for 60 seconds.

Prices and online payment

The free plugin shows a price per seat and takes payment on site. What a booking costs is worked out once by happenboard_booking_price, then stored on the booking with its currency. Emails, the Bookings screen and the checkout read that stored amount, even if prices change later.

HappenBoard Pro takes payment through Stripe. It answers happenboard_booking_awaiting_payment (pay now) or, for a request, happenboard_booking_authorize_payment (hold the card until you approve). It then sends the customer to Stripe through happenboard_booking_payment_url and charges a held card through happenboard_booking_capture_payment.

When less than the price is taken, the booking is recorded as a deposit, with the rest due on site. How long Pro keeps a card held before declining the request is set by happenboard_pro_booking_hold_hours.

happenboard_booking_slots

Filter Free src/Service/Booking/Availability/SlotGenerator.php

Filters the free times of a booking type, after availability rules, notice limits, buffers, daily caps and capacity are applied. Every offered time passes here, so it is the one place to add or remove bookable slots.

Signature

apply_filters( 'happenboard_booking_slots', array $list, int $booking_type_id, string $from, string $to, array $context )

Parameters

$list array<int,array<string,mixed>>
Slots sorted by start. Each has start_utc and end_utc (UTC Y-m-d H:i:s), start (ISO 8601 in the visitor’s timezone), the display strings label, day_label and time_label, hosts (user IDs free then), host_seats (host ID to seats left) and capacity_left (the most seats any host has).
$booking_type_id int
The booking type (hboard_booking_type post ID).
$from string
First day of the window, as the engine received it (a site-timezone Y-m-d on the public route).
$to string
Last day of the window.
$context array{length:int,ignore_booking_id:int}
length: how many of the type’s durations each slot runs. ignore_booking_id: a booking being moved, whose own seats must not count, or 0.

Return value

array<int,array<string,mixed>> A zero-indexed list of slots.

Example

add_filter(
    'happenboard_booking_slots',
    function ( $slots, $booking_type_id ) {
        if ( 42 !== $booking_type_id ) {
            return $slots;
        }

        // No bookings on Friday afternoons, site time.
        $slots = array_filter(
            $slots,
            function ( $slot ) {
                $local = ( new DateTimeImmutable( $slot['start_utc'], new DateTimeZone( 'UTC' ) ) )->setTimezone( wp_timezone() );
                return ! ( '5' === $local->format( 'N' ) && (int) $local->format( 'G' ) >= 12 );
            }
        );

        return array_values( $slots );
    },
    10,
    2
);

Notes

  • Up to five arguments: use add_filter( ..., 10, 5 ) to receive $context. A callback that subtracts its own usage should leave out the booking named in ignore_booking_id, as the engine does.
  • It also decides what can be written. Making or moving a booking runs the engine again, uncached, and refuses a start missing from your list. A slot you add needs start_utc in UTC Y-m-d H:i:s, real user IDs in hosts and enough seats in host_seats or capacity_left.
  • Return a list: finish with array_values(). A list with gaps reaches the browser as a JSON object and breaks the Booking Page.
  • On the write path the window is one day and the display strings are in UTC, so base your logic on start_utc. HappenBoard Pro hooks in at priority 10 (shared resources), 20 (external calendars) and 30 (a price per slot): use a priority above 30 to see the final list.

happenboard_slot_cache_ttl

Filter Free src/Service/Booking/Availability/SlotCache.php

Filters how long the public slot list of a booking type is cached, in seconds. By default lists are cached for 5 minutes when the site has a persistent object cache, and not at all otherwise.

Signature

apply_filters( 'happenboard_slot_cache_ttl', int $ttl )

Parameters

$ttl int
300 with a persistent object cache, 0 without.

Return value

int Seconds to cache. 0 turns caching off, for reads and writes.

Example

add_filter(
    'happenboard_slot_cache_ttl',
    function ( $ttl ) {
        // A busy shared calendar: keep lists one minute at most.
        return $ttl > 0 ? MINUTE_IN_SECONDS : 0;
    }
);

Notes

  • Without a persistent object cache, a positive value stores each list in wp_options (often 60 to 230 KB per entry). Those rows stay behind after every booking change, until WordPress deletes expired transients. Keep 0 there.
  • The cache only speeds up display. The time a visitor picks is checked again, uncached, when the booking is written: a stale list can offer a time that is then refused, never double-book it.
  • Every list is dropped when a booking is created, confirmed, cancelled, moved or released unpaid, and when a booking type or its settings change. Entries are keyed by booking type, window, timezone, language and length.
  • The cached list is the one returned by happenboard_booking_slots, so your slot filter’s answer is cached too.

happenboard_slots_max_age

Filter Free src/REST/BookingSlots.php

Filters how long, in seconds, a browser or CDN may reuse the slot list sent to an anonymous visitor. The public slots route sends Cache-Control: public, max-age=60 by default.

Signature

apply_filters( 'happenboard_slots_max_age', int $max_age )

Parameters

$max_age int
60 by default.

Return value

int Seconds. 0 sends no Cache-Control header.

Example

add_filter(
    'happenboard_slots_max_age',
    function () {
        // Our CDN caches REST answers: keep slot lists fresher.
        return 15;
    }
);

Notes

  • Applies to GET /happenboard/v1/booking-types/{id}/slots for visitors who are not logged in. Logged-in requests, the times offered to move a booking and a 429 refusal are never marked cacheable.
  • A reused list can still show a time someone has just booked. The booking is checked again when written, so that visitor is told the time is gone and nobody is double-booked.
  • A CDN is not purged when a booking is made: this value is the longest a reused list can lag behind.

happenboard_booking_location_labels

Filter Free and Pro src/Block/BookingPage/Block.php

Filters the words shown for a booking type’s location mode, such as “In person”. Use it to name the custom mode, which has no label by default, or to rename the others.

Signature

apply_filters( 'happenboard_booking_location_labels', array $labels )

Parameters

$labels array<string,string>
Mode slug to label: in_person (In person), virtual (Online), phone (By phone).

Return value

array<string,string> The labels. A mode without one shows no location.

Example

add_filter(
    'happenboard_booking_location_labels',
    function ( $labels ) {
        $labels['custom'] = 'At your premises';

        return $labels;
    }
);

Notes

  • Used by the Booking Page and Booking Availability blocks, and by the results of HappenBoard Pro’s Find a Time block.
  • Labels are escaped when printed: return plain text.

happenboard_booking_page_choices

Filter Free src/Block/BookingPage/Block.php

Adds pickers such as “Room” or “Table” to the Booking Page block, shown before the visitor picks a time. Each picker narrows the times shown and is sent with the booking.

Signature

apply_filters( 'happenboard_booking_page_choices', array $choices, array $config )

Parameters

$choices array<int,array<string,mixed>>
Pickers so far. On a fixed booking type with several hosts, the free plugin adds a host picker (key host, field hosts).
$config array<string,mixed>
Resolved booking-type config, including id.

Return value

array<int,array<string,mixed>> Pickers, each with key (sent as choices[key]), field (the slot key listing the IDs free at that time), label, any (the no-preference label, value 0) and options (value and label pairs).

Example

add_filter(
    'happenboard_booking_page_choices',
    function ( $choices, $config ) {
        if ( 42 !== (int) $config['id'] ) {
            return $choices;
        }

        $choices[] = array(
            'key'     => 'table',
            'field'   => 'free_tables', // Added to each slot by your slots filter.
            'label'   => 'Table',
            'any'     => 'Any table',
            'options' => array( array( 'value' => 1, 'label' => 'Window' ), array( 'value' => 2, 'label' => 'Terrace' ) ),
        );
        return $choices;
    },
    10,
    2
);

Notes

  • Two arguments: register with add_filter( ..., 10, 2 ).
  • A time is shown only when its slot lists the chosen ID under field: add that list to each slot in happenboard_booking_slots.
  • Check and settle the posted value in happenboard_booking_choices: anyone can post any value to the REST route.
  • HappenBoard Pro adds its resource picker here (key resource, field resources) when a booking type offers the customer a choice of two or more active resources.

happenboard_booking_person_types

Filter Free src/Service/Booking/PersonTypes.php

Defines the person types a booking type takes, such as Adult and Child, so visitors book “2 × Adult, 1 × Child” instead of a seat count. The free plugin defines none; HappenBoard Pro returns the types set on the booking type.

Signature

apply_filters( 'happenboard_booking_person_types', array $types, array $config )

Parameters

$types array<int,array<string,mixed>>
Empty by default.
$config array<string,mixed>
Resolved booking-type config, including id.

Return value

array<int,array<string,mixed>> Types, each with key, label, min, max (0 for no limit of its own) and seats (whether each person takes a seat).

Example

add_filter(
    'happenboard_booking_person_types',
    function ( $types, $config ) {
        if ( array() !== $types || 42 !== (int) $config['id'] ) {
            return $types;
        }

        return array(
            array( 'key' => 'adult', 'label' => 'Adult', 'min' => 1, 'max' => 0, 'seats' => true ),
            array( 'key' => 'child', 'label' => 'Child', 'min' => 0, 'max' => 4, 'seats' => true ),
            // Infants sit on a lap: no seat taken.
            array( 'key' => 'infant', 'label' => 'Infant', 'min' => 0, 'max' => 2, 'seats' => false ),
        );
    },
    5,
    2
);

Notes

  • Two arguments: register with add_filter( ..., 10, 2 ). HappenBoard Pro answers at priority 10 and keeps a non-empty list it receives, so a lower priority wins.
  • The list is cleaned: at most 10 types, counts up to 50, labels up to 60 characters, and seats true unless set to false.
  • Once a booking type has person types, a visitor’s booking must send counts or it is refused (happenboard_booking_persons_invalid, HTTP 400). The seats booked become the people who take a seat, at least 1.
  • The breakdown is stored on the booking, labels included, and passed to happenboard_booking_price and happenboard_booking_choices. The free plugin still prices per seat; prices per type come from Pro.

happenboard_booking_price

Filter Free src/Service/Booking/BookingPrice.php

Filters what a booking costs, in minor units of the site currency (cents for USD or EUR). The free plugin’s answer is the booking type’s price times the seats booked.

Signature

apply_filters( 'happenboard_booking_price', int $price, array $config, array $booking )

Parameters

$price int
Flat price times seats, in minor units.
$config array<string,mixed>
Resolved booking-type config.
$booking array{start_utc:string,host_user_id:int,quantity:int,length:int,persons:array<string,int>}
start_utc, host_user_id (0 when no host is chosen yet), quantity (seats), length (how many durations, 1 on a fixed-length type) and persons (person type key to count).

Return value

int The price in minor units. A negative value becomes 0.

Example

add_filter(
    'happenboard_booking_price',
    function ( $price, $config, $booking ) {
        $start = new DateTimeImmutable( $booking['start_utc'], new DateTimeZone( 'UTC' ) );

        // 20% evening supplement from 18:00, site time.
        if ( (int) $start->setTimezone( wp_timezone() )->format( 'G' ) >= 18 ) {
            $price = (int) round( $price * 1.2 );
        }

        return $price;
    },
    20,
    3
);

Notes

  • Three arguments: register with add_filter( ..., 10, 3 ).
  • The result is stored on the booking (price_cents and currency) when it is made. Emails, the Bookings screen and the checkout use that amount, even if your rule changes later.
  • The public quote route (GET /happenboard/v1/booking-types/{id}/quote) asks it too, so the Booking Page shows the amount the booking is then written with. Keep it free of side effects.
  • HappenBoard Pro applies its pricing rules at priority 10 and, with person types, prices each type and ignores the incoming amount. Hook later than 10 to adjust its result.

happenboard_booking_price_label

Filter Free src/Service/Booking/BookingPrice.php

Filters the headline price shown for a booking type before a time is picked, in the Booking Page, Booking Availability and Booking Types blocks.

Signature

apply_filters( 'happenboard_booking_price_label', string $label, array $config )

Parameters

$label string
The flat price formatted in the site currency, or '' when it is 0.
$config array<string,mixed>
Resolved booking-type config.

Return value

string Plain text, or '' to show no price.

Example

add_filter(
    'happenboard_booking_price_label',
    function ( $label, $config ) {
        // A service priced after a first consultation.
        return 42 === (int) $config['id'] ? 'On quote' : $label;
    },
    20,
    2
);

Notes

  • Two arguments: register with add_filter( ..., 10, 2 ).
  • It changes the words only. The amount owed or charged comes from happenboard_booking_price.
  • HappenBoard Pro returns a “From” price at priority 10 when its pricing rules make the price depend on the time.
  • The blocks escape the label: return plain text, not HTML.

happenboard_currency

Filter Free src/Service/Money.php

Filters the site currency, as a three-letter ISO 4217 code. It is the currency of every booking price HappenBoard shows and records.

Signature

apply_filters( 'happenboard_currency', string $code )

Parameters

$code string
The currency saved in HappenBoard’s general settings, USD by default.

Return value

string A three-letter code. Anything else falls back to USD.

Example

add_filter(
    'happenboard_currency',
    function ( $code ) {
        // Follow the WooCommerce store currency when WooCommerce is active.
        return function_exists( 'get_woocommerce_currency' ) ? get_woocommerce_currency() : $code;
    }
);

Notes

  • Nothing is converted: switching currency relabels the prices of your booking types.
  • Each booking stores the currency it was made in and keeps showing it, so a later switch does not relabel bookings already made.
  • HappenBoard Pro turns online booking payment off when its Stripe currency uses a different number of decimals than this one (USD and JPY, for example).
  • Read the currency with \HappenBoard\Service\Money::currency(), not the raw setting, so your code and the plugin agree.

happenboard_booking_client_ip

Filter Free and Pro src/REST/Bookings.php

Filters the visitor IP address that the public booking routes count requests against. Use it behind a reverse proxy or CDN, where every visitor otherwise arrives from the proxy’s address.

Signature

apply_filters( 'happenboard_booking_client_ip', string $ip )

Parameters

$ip string
REMOTE_ADDR when it is a valid IP, else ''.

Return value

string An IP address, or '' to count nothing.

Example

add_filter(
    'happenboard_booking_client_ip',
    function ( $ip ) {
        // Behind Cloudflare: use the visitor address it forwards.
        $real = filter_var( $_SERVER['HTTP_CF_CONNECTING_IP'] ?? '', FILTER_VALIDATE_IP );

        return $real ? $real : $ip;
    }
);

Notes

  • Read by booking submissions (8 a minute per address, 5 an hour per email) and HappenBoard Pro’s Find a Time search (20 a minute). Slot and quote lookups allow 60 a minute, counted only with a persistent object cache unless happenboard_throttle_public_reads says otherwise.
  • On a submission, the same address is sent to Cloudflare Turnstile and Akismet when they are turned on.
  • Only trust a header your proxy sets. If the site can also be reached directly, anyone can send that header and choose their own address.
  • Returning '' turns the per-address limits off. RSVPs and ticket checkouts read happenboard_rsvp_client_ip instead: add the same callback there.

happenboard_booking_choices

Filter Free src/Service/Booking/BookingService.php

Checks and settles the extension choices of a booking about to be written, such as a room picked on the Booking Page. Return a WP_Error to refuse the booking or the move.

Signature

apply_filters( 'happenboard_booking_choices', array $choices, array $context, array $slot )

Parameters

$choices array<string,int>|WP_Error
Choice key to ID from the booking request (0 means no preference). Empty when a booking is moved. May be a WP_Error from an earlier callback.
$context array<string,mixed>
action (create or reschedule), booking_type_id, booking_id (0 on create), start_utc, end_utc, host_user_id, quantity, length and persons.
$slot array<string,mixed>
The live slot, never a cached one, with whatever your happenboard_booking_slots callback added.

Return value

array<string,int>|WP_Error The settled choices, or a WP_Error with a status such as 409.

Example

add_filter(
    'happenboard_booking_choices',
    function ( $choices, $context, $slot ) {
        if ( is_wp_error( $choices ) || ! isset( $choices['table'] ) ) {
            return $choices;
        }

        // 'free_tables' was added to each slot by your slots filter.
        $free   = array_map( 'intval', (array) ( $slot['free_tables'] ?? array() ) );
        $wanted = (int) $choices['table'];
        if ( array() === $free || ( $wanted > 0 && ! in_array( $wanted, $free, true ) ) ) {
            return new WP_Error( 'acme_table_taken', 'That table is no longer free.', array( 'status' => 409 ) );
        }

        // Settle "no preference" on the first free table.
        $choices['table'] = $wanted > 0 ? $wanted : $free[0];
        return $choices;
    },
    10,
    3
);

Notes

  • Three arguments: register with add_filter( ..., 10, 3 ). Pass on a WP_Error you receive.
  • It runs inside the booking write lock, after the free plugin’s own checks: keep it fast.
  • The free plugin does not store choices. The settled values reach happenboard_booking_created (third argument) and happenboard_booking_rescheduled (fourth): save them there.
  • The public route accepts up to 10 choices, keys through sanitize_key() and IDs as whole numbers. HappenBoard Pro settles its resource choice here at priority 10.

happenboard_booking_assignment_strategy

Filter Free src/Service/Booking/BookingService.php

Replaces the object that picks the host of a new booking among the hosts free at that time. It also runs when a moved booking’s host is not free at the new time.

Signature

apply_filters( 'happenboard_booking_assignment_strategy', \HappenBoard\Service\Booking\Assignment\Strategy $strategy, string $assignment )

Parameters

$strategy \HappenBoard\Service\Booking\Assignment\Strategy
LeastLoaded (the host with the fewest upcoming bookings) for round_robin, Fixed (the chosen host, else the first free one) for any other mode.
$assignment string
The booking type’s mode: fixed or round_robin, the two the booking type editor offers.

Return value

\HappenBoard\Service\Booking\Assignment\Strategy An object whose pick( array $candidate_host_ids, int $booking_type_id, string $slot_utc ) returns a user ID, or 0 for none.

Example

add_filter(
    'happenboard_booking_assignment_strategy',
    function ( $strategy, $assignment ) {
        if ( 'round_robin' !== $assignment ) {
            return $strategy;
        }

        // Give the booking to host 12 whenever they are free.
        return new class( $strategy ) implements \HappenBoard\Service\Booking\Assignment\Strategy {
            public function __construct( private $fallback ) {}

            public function pick( array $candidate_host_ids, int $booking_type_id, string $slot_utc ): int {
                return in_array( 12, $candidate_host_ids, true )
                    ? 12
                    : $this->fallback->pick( $candidate_host_ids, $booking_type_id, $slot_utc );
            }
        };
    },
    10,
    2
);

Notes

  • Two arguments: register with add_filter( ..., 10, 2 ). Returning anything other than a Strategy throws a TypeError.
  • Candidates are the hosts with enough seats free at that time, or only the host the visitor chose when that host is free. The ID pick() returns is not checked again: return a candidate, or 0 to refuse the booking (happenboard_booking_no_host, HTTP 409).
  • On a move, a fixed booking type never reaches it: the move is refused when the booking’s own host is busy (happenboard_booking_host_unavailable, HTTP 409).
  • Only round_robin has a strategy of its own. Other stored modes (collective, weighted, priority) behave like fixed, and HappenBoard Pro adds no strategy today.

happenboard_booking_awaiting_payment

Filter Free src/Service/Booking/BookingService.php

Decides whether a booking must be paid online before it counts. The free plugin answers no (payment on site); HappenBoard Pro answers yes when online booking payment is on and Stripe is set up.

Signature

apply_filters( 'happenboard_booking_awaiting_payment', bool $awaiting, int $booking_type_id, array $config, int $price_cents )

Parameters

$awaiting bool
false by default.
$booking_type_id int
The booking type being booked.
$config array<string,mixed>
Resolved booking-type config, including price_cents (flat price per seat) and deposit_cents.
$price_cents int
What this booking costs, from happenboard_booking_price.

Return value

bool True to write the booking as pending_payment, holding its time while the customer pays.

Example

add_filter(
    'happenboard_booking_awaiting_payment',
    function ( $awaiting, $booking_type_id, $config, $price_cents ) {
        // Small amounts are paid on site, even with online payment on.
        return $awaiting && $price_cents >= 5000;
    },
    20,
    4
);

Notes

  • Four arguments: register with add_filter( ..., 10, 4 ).
  • Asked only for a priced booking made by a visitor, on a booking type that does not need approval. Requests go through happenboard_booking_authorize_payment instead.
  • The Booking Page and the quote route ask it too, to say how the price will be paid: keep it free of side effects.
  • Returning true with no URL from happenboard_booking_payment_url releases the time at once, and the visitor gets happenboard_booking_payment_unavailable (HTTP 502).

happenboard_booking_authorize_payment

Filter Free src/Service/Booking/BookingService.php

Decides whether a booking request, on a type that needs approval, must have the customer’s card held online before it reaches your approval queue. The card is charged when you approve and released when you decline.

Signature

apply_filters( 'happenboard_booking_authorize_payment', bool $authorize, int $booking_type_id, array $config, int $price_cents )

Parameters

$authorize bool
false by default.
$booking_type_id int
The booking type being booked.
$config array<string,mixed>
Resolved booking-type config, including price_cents (flat price per seat) and deposit_cents.
$price_cents int
What this booking costs, from happenboard_booking_price.

Return value

bool True to send the customer to a checkout that holds the card. The booking holds its time as pending_payment meanwhile.

Example

add_filter(
    'happenboard_booking_authorize_payment',
    function ( $authorize, $booking_type_id, $config, $price_cents ) {
        // Only hold a card for requests worth 100.00 or more.
        return $authorize && $price_cents >= 10000;
    },
    20,
    4
);

Notes

  • Four arguments: register with add_filter( ..., 10, 4 ).
  • Asked only for a priced request made by a visitor. A booking entered by an operator is confirmed at once: the operator is the approval.
  • Kept apart from happenboard_booking_awaiting_payment on purpose: a provider that can only charge outright would otherwise charge requests in full.
  • HappenBoard Pro answers it with the same rule as happenboard_booking_awaiting_payment. The Booking Page and quote route ask it too: keep it free of side effects.

happenboard_booking_payment_url

Filter Free src/Service/Booking/BookingService.php

Filters the address a customer is sent to after a booking is written as pending_payment, to pay or to have their card held. HappenBoard Pro returns a Stripe Checkout URL.

Signature

apply_filters( 'happenboard_booking_payment_url', string $url, int $booking_id, array $booking )

Parameters

$url string
Empty by default.
$booking_id int
Booking ID.
$booking array<string,mixed>
The saved booking, cancel_token removed. payment_status is pending (charge now) or authorizing (hold the card only); price_cents is what it costs.

Return value

string An absolute URL, or ''.

Example

add_filter(
    'happenboard_booking_payment_url',
    function ( $url, $booking_id, $booking ) {
        // Keep an earlier answer, else build our own checkout link.
        return '' !== $url ? $url : acme_checkout_link( $booking_id, (int) $booking['price_cents'] );
    },
    20,
    3
);

Notes

  • Up to three arguments: register with add_filter( ..., 10, 3 ) to receive the row.
  • An empty answer cancels the booking at once, puts the time back on offer and answers the visitor with happenboard_booking_payment_unavailable (HTTP 502). No action fires.
  • For authorizing, the checkout must only hold the card (Stripe’s capture_method manual), never charge it.
  • HappenBoard Pro’s checkout expires after 31 minutes. Its webhook then frees the time and happenboard_booking_payment_abandoned fires.

happenboard_booking_capture_payment

Filter Free src/Service/Booking/BookingService.php

Charges the card held for a booking request when you approve it. The request is confirmed only once this reports the money taken.

Signature

apply_filters( 'happenboard_booking_capture_payment', array|WP_Error|null $result, int $booking_id, array $booking )

Parameters

$result array<string,mixed>|WP_Error|null
null by default, or an earlier callback’s answer.
$booking_id int
Booking ID.
$booking array<string,mixed>
The booking, cancel_token removed. payment_intent holds the provider reference saved with the hold, paid_cents the amount held.

Return value

array<string,mixed>|WP_Error|null array( 'amount' => int ) once charged (in minor units), a WP_Error saying why not, or null when nothing can charge it.

Example

add_filter(
    'happenboard_booking_capture_payment',
    function ( $result, $booking_id, $booking ) {
        $reference = (string) ( $booking['payment_intent'] ?? '' );

        // Leave answered results, and holds from other gateways, alone.
        if ( null !== $result || ! str_starts_with( $reference, 'acme_' ) ) {
            return $result;
        }

        $charge = acme_gateway_capture( $reference ); // Must be safe to retry.

        return is_wp_error( $charge ) ? $charge : array( 'amount' => (int) $charge['amount'] );
    },
    5,
    3
);

Notes

  • Three arguments: register with add_filter( ..., 10, 3 ). HappenBoard Pro captures the Stripe hold at priority 10: pass a non-null $result on unless you mean to replace it.
  • null: the approval is refused (HTTP 503) and nothing changes. A WP_Error: the request stays in the queue, a note records the reason and the approval answers HTTP 402. An array: the booking is confirmed.
  • Without amount, the held amount is recorded as taken. Less than price_cents is stored as a deposit.
  • After a failure the operator can approve again, which asks this filter again: make the charge safe to retry.

happenboard_booking_payment_hold_hours

Filter Free src/Service/Booking/PaymentHoldSweeper.php

Filters how long an unpaid booking may hold its time before the hourly clean-up releases it. It is the safety net for a payment webhook that never arrives.

Signature

apply_filters( 'happenboard_booking_payment_hold_hours', int $hours )

Parameters

$hours int
24 by default.

Return value

int Hours. Values below 1 become 1.

Example

add_filter(
    'happenboard_booking_payment_hold_hours',
    function () {
        // Never shorter than the payment provider's checkout session.
        return 6;
    }
);

Notes

  • Never set it below your provider’s checkout lifetime, or a customer can pay for a time already given away. HappenBoard Pro’s Stripe checkout lasts 31 minutes.
  • Age is counted from when the booking was made, not from its start. Each run releases up to 500 bookings still pending_payment and fires happenboard_booking_payment_abandoned for each.
  • It runs on WP-Cron (happenboard_release_payment_holds, hourly), so a site with little traffic releases late.
  • Not to be confused with happenboard_pro_booking_hold_hours, which declines requests whose card has been held too long.

happenboard_booking_created

Action Free src/Service/Booking/BookingService.php

Fires after a new booking is saved, whatever its status. Use it to sync bookings to a CRM or create a meeting link.

Signature

do_action( 'happenboard_booking_created', int $booking_id, array $data, array $choices )

Parameters

$booking_id int
Booking ID.
$data array<string,mixed>
The saved row, with cancel_token removed (never pass that secret on: anyone holding it can cancel or move the booking).
$choices array<string,int>
Extension choices as settled by happenboard_booking_choices.

Example

add_action(
    'happenboard_booking_created',
    function ( $booking_id, $data, $choices ) {
        wp_remote_post(
            'https://crm.example.com/bookings',
            array(
                'timeout' => 5,
                'headers' => array( 'Content-Type' => 'application/json' ),
                'body'    => wp_json_encode( array(
                    'id'     => (int) $booking_id,
                    'status' => (string) $data['status'],
                    'start'  => (string) $data['start_utc'],
                    'email'  => (string) $data['customer_email'],
                ) ),
            )
        );
    },
    10,
    3
);

Notes

  • Three arguments: register with add_action( ..., 10, 3 ).
  • status can be confirmed, pending_approval or pending_payment. For real appointments only, listen on happenboard_booking_confirmed, which also covers bookings confirmed later.
  • Values arrive as database strings: cast before comparing, and read persons with \HappenBoard\Service\Booking\PersonTypes::from_row().
  • Already hooked: HappenBoard Pro at priority 5 saves the resource in $choices; the free notifier at 10 sends the confirmation or “request received” email, and nothing yet for a booking awaiting payment.

happenboard_booking_payment_pending

Action Free src/Service/Booking/BookingService.php

Fires when a new booking holds its time while the customer pays online, or while their card is held for a request.

Signature

do_action( 'happenboard_booking_payment_pending', int $booking_id )

Parameters

$booking_id int
Booking ID.

Example

add_action(
    'happenboard_booking_payment_pending',
    function ( $booking_id ) {
        acme_metrics( 'booking_checkout_started', $booking_id );
    }
);

Notes

  • The row’s payment_status tells the two apart: pending (paying now) or authorizing (card to be held).
  • It is not a commitment: when happenboard_booking_payment_url returns nothing right after, the booking is cancelled on the spot and nothing more fires.
  • Named _payment_pending because happenboard_booking_awaiting_payment is a filter, and WordPress keeps actions and filters in one list.

happenboard_booking_requested

Action Free src/Service/Booking/BookingService.php

Fires when a booking request reaches your approval queue. Use it to alert whoever has to answer.

Signature

do_action( 'happenboard_booking_requested', int $booking_id )

Parameters

$booking_id int
Booking ID.

Example

add_action(
    'happenboard_booking_requested',
    function ( $booking_id ) {
        acme_slack( sprintf( 'Booking request #%d is waiting for an answer.', $booking_id ) );
    }
);

Notes

  • Fires from two places: at creation for a request with nothing to pay online, and right after happenboard_booking_payment_authorized once the customer’s card is held.
  • happenboard_booking_confirmed does not fire for a request. It fires later, if you approve it.
  • Already hooked: the free notifier emails the host and the site admin that a request is waiting.

happenboard_booking_confirmed

Action Free src/Service/Booking/BookingService.php

Fires whenever a booking becomes confirmed, on every path: booked directly, paid online or approved. Listen here for anything that should happen once per real appointment.

Signature

do_action( 'happenboard_booking_confirmed', int $booking_id )

Parameters

$booking_id int
Booking ID.

Example

add_action(
    'happenboard_booking_confirmed',
    function ( $booking_id ) {
        $booking = ( new \HappenBoard\Service\Booking\BookingRepository() )->find_by_id( (int) $booking_id );
        if ( null === $booking ) {
            return;
        }

        // Count confirmed appointments per host.
        $host = (int) $booking['host_user_id'];
        update_user_meta( $host, 'acme_confirmed_count', (int) get_user_meta( $host, 'acme_confirmed_count', true ) + 1 );
    }
);

Notes

  • It follows happenboard_booking_created, happenboard_booking_paid or happenboard_booking_approved, depending on the path.
  • Only the ID is passed. The row’s payment_status says how it is paid: none, on_site, paid or deposit.
  • Already hooked: the free notifier emails the host and the site admin. The customer’s confirmation goes out from happenboard_booking_created, happenboard_booking_approved or happenboard_booking_paid.

happenboard_booking_approved

Action Free src/Service/Booking/BookingService.php

Fires when a booking request is approved and confirmed. For a request with a card held, it fires only once that card is charged.

Signature

do_action( 'happenboard_booking_approved', int $booking_id )

Parameters

$booking_id int
Booking ID.

Example

add_action(
    'happenboard_booking_approved',
    function ( $booking_id ) {
        acme_log( 'booking_approved', $booking_id );
    }
);

Notes

  • happenboard_booking_confirmed fires right after. Approving a booking that is not awaiting approval is refused (HTTP 409), so it never fires twice for one booking.
  • Operators approve with PATCH /happenboard/v1/bookings/{id} and status=confirmed. It also fires when the payment provider reports a charge made elsewhere, such as from the Stripe dashboard.
  • Already hooked: the free notifier sends the customer the confirmation, with its calendar invite, and schedules the reminders.

happenboard_booking_paid

Action Free src/Service/Booking/BookingService.php

Fires when the online payment for a booking lands and the booking is confirmed. HappenBoard Pro reaches it from its Stripe webhook.

Signature

do_action( 'happenboard_booking_paid', int $booking_id )

Parameters

$booking_id int
Booking ID.

Example

add_action(
    'happenboard_booking_paid',
    function ( $booking_id ) {
        acme_issue_receipt( $booking_id );
    }
);

Notes

  • happenboard_booking_confirmed fires right after. A webhook delivered twice confirms once, so it fires once per booking.
  • The amount taken is stored in paid_cents. Less than price_cents is recorded as a deposit (payment_status deposit), the rest due on site.
  • A held card charged on approval does not fire it: see happenboard_booking_payment_captured.
  • Already hooked: the free notifier sends the confirmation email and schedules the reminders.

happenboard_booking_payment_authorized

Action Free src/Service/Booking/BookingService.php

Fires when the customer’s card is held for a booking request and the request reaches your approval queue. Nothing is charged yet.

Signature

do_action( 'happenboard_booking_payment_authorized', int $booking_id )

Parameters

$booking_id int
Booking ID.

Example

add_action(
    'happenboard_booking_payment_authorized',
    function ( $booking_id ) {
        acme_slack( sprintf( 'Request #%d is waiting, card held.', $booking_id ) );
    }
);

Notes

  • happenboard_booking_requested fires right after. The booking is now pending_approval, with payment_status authorized and the amount held in paid_cents.
  • Already hooked: the free notifier sends the customer the “request received” email, held back while they were at the checkout.

happenboard_booking_payment_captured

Action Free src/Service/Booking/BookingService.php

Fires when the card held for a booking request has been charged and the booking confirmed.

Signature

do_action( 'happenboard_booking_payment_captured', int $booking_id )

Parameters

$booking_id int
Booking ID.

Example

add_action(
    'happenboard_booking_payment_captured',
    function ( $booking_id ) {
        acme_issue_receipt( $booking_id );
    }
);

Notes

  • Fires last, after happenboard_booking_approved and happenboard_booking_confirmed, whether you approved the request or the provider reported a charge made elsewhere.
  • happenboard_booking_paid does not fire here, so the customer gets one confirmation email.
  • The amount is in paid_cents. Less than price_cents is recorded as a deposit.

happenboard_booking_payment_failed

Action Free src/Service/Booking/BookingService.php

Fires the first time the card held for a booking request cannot be charged when you approve it. The request stays in the queue, its time still held.

Signature

do_action( 'happenboard_booking_payment_failed', int $booking_id, WP_Error $error )

Parameters

$booking_id int
Booking ID.
$error WP_Error
Why the charge was refused, as returned by happenboard_booking_capture_payment.

Example

add_action(
    'happenboard_booking_payment_failed',
    function ( $booking_id, $error ) {
        error_log( sprintf( 'Charge failed for booking %d: %s', $booking_id, $error->get_error_message() ) );
    },
    10,
    2
);

Notes

  • Two arguments: register with add_action( ..., 10, 2 ).
  • Once only: approving again retries the card, but a second failure fires nothing. Each failure still adds a note to the booking.
  • Already hooked: the free notifier tells the customer their card could not be charged.

happenboard_booking_payment_released

Action Free src/Service/Booking/BookingService.php

Fires when the card held for a booking is released and nothing was taken. The booking’s payment_status becomes voided.

Signature

do_action( 'happenboard_booking_payment_released', int $booking_id )

Parameters

$booking_id int
Booking ID.

Example

add_action(
    'happenboard_booking_payment_released',
    function ( $booking_id ) {
        acme_log( 'card_hold_released', $booking_id );
    }
);

Notes

  • HappenBoard Pro releases the card when a request with a held card is declined or cancelled, and when Stripe reports the hold cancelled or expired.
  • Fires only when a card was really held (payment_status authorized or failed), never for a paid booking.
  • No email is sent here: the customer hears about the decline or the cancellation itself.

happenboard_booking_payment_abandoned

Action Free src/Service/Booking/BookingService.php

Fires when a booking whose payment never arrived is released and its time goes back on offer. It covers a checkout to pay and a checkout to hold a card.

Signature

do_action( 'happenboard_booking_payment_abandoned', int $booking_id )

Parameters

$booking_id int
Booking ID.

Example

add_action(
    'happenboard_booking_payment_abandoned',
    function ( $booking_id ) {
        acme_metrics( 'booking_checkout_abandoned', $booking_id );
    }
);

Notes

  • Two sources: HappenBoard Pro’s Stripe webhook when the checkout expires or a delayed payment fails, and the free plugin’s hourly clean-up for holds older than happenboard_booking_payment_hold_hours.
  • The status becomes cancelled, but happenboard_booking_cancelled does not fire: the customer never had a booking, so no cancellation email goes out.

happenboard_booking_declined

Action Free src/Service/Booking/BookingService.php

Fires when a booking request is declined, just before happenboard_booking_cancelled. The booking’s status is now declined.

Signature

do_action( 'happenboard_booking_declined', int $booking_id )

Parameters

$booking_id int
Booking ID.

Example

add_action(
    'happenboard_booking_declined',
    function ( $booking_id ) {
        acme_log( 'booking_declined', $booking_id );
    }
);

Notes

  • Reached when you decline a request (PATCH /happenboard/v1/bookings/{id} with status=declined), when you cancel a request still awaiting approval, and when HappenBoard Pro declines a request whose card hold lapsed or is about to.
  • Already hooked: the free notifier sends the customer the “request declined” email and skips the cancellation email.
  • When a card was held, HappenBoard Pro releases it on happenboard_booking_cancelled, and happenboard_booking_payment_released follows.

happenboard_booking_cancelled

Action Free src/Service/Booking/BookingService.php

Fires when a booking is cancelled by the customer or an operator, and after every decline. Use it to free anything your extension reserved for the booking.

Signature

do_action( 'happenboard_booking_cancelled', int $booking_id )

Parameters

$booking_id int
Booking ID.

Example

add_action(
    'happenboard_booking_cancelled',
    function ( $booking_id ) {
        $booking = ( new \HappenBoard\Service\Booking\BookingRepository() )->find_by_id( (int) $booking_id );

        // Free the meeting room this extension booked, for both cases.
        if ( null !== $booking && in_array( $booking['status'], array( 'cancelled', 'declined' ), true ) ) {
            acme_release_room( (int) $booking_id );
        }
    }
);

Notes

  • Read the row’s status to tell a cancellation (cancelled) from a declined request (declined).
  • Cancelling twice is refused (HTTP 409) and fires nothing. Customers cannot cancel after the booking type’s cancellation cutoff; operators can.
  • It does not fire for a checkout never completed (happenboard_booking_payment_abandoned) or for a booking released because no payment URL came back.
  • Already hooked: HappenBoard Pro at priority 5 releases a held card. The free notifier at 10 sends the cancellation email (not after a decline), tells the host and site admin, and clears the reminders.

happenboard_booking_rescheduled

Action Free src/Service/Booking/BookingService.php

Fires after a booking moves to a new time. The new time has passed the same slot, seat and host checks as a new booking.

Signature

do_action( 'happenboard_booking_rescheduled', int $booking_id, string $old_start, string $new_start, array $choices )

Parameters

$booking_id int
Booking ID.
$old_start string
Previous start, UTC Y-m-d H:i:s.
$new_start string
New start, UTC Y-m-d H:i:s. The end is not passed: read it from the row.
$choices array<string,int>
Extension choices as settled by happenboard_booking_choices for the move.

Example

add_action(
    'happenboard_booking_rescheduled',
    function ( $booking_id, $old_start, $new_start ) {
        // Both times are UTC 'Y-m-d H:i:s'.
        acme_crm_update( $booking_id, array( 'start_utc' => $new_start, 'previous_start_utc' => $old_start ) );
    },
    10,
    3
);

Notes

  • Up to four arguments: register with 10, 3 for the times, or 10, 4 to receive $choices.
  • The host can change: on a booking type that assigns hosts itself, a busy host is replaced by a free one. Read host_user_id from the row.
  • Customers move their booking from the link in their emails (confirmed bookings and requests, before the cancellation cutoff). Operators use the reschedule-booking ability, with no cutoff.
  • Already hooked: HappenBoard Pro at priority 5 moves the booking’s resource. The free notifier at 10 re-schedules the reminders, sends the customer a “booking moved” email and tells the host and site admin.

happenboard_rest_booking_response

Filter Free src/REST/Bookings.php

Filters one booking as the admin Bookings screen receives it. Use it to add a field, usually shown as an extra column.

Signature

apply_filters( 'happenboard_rest_booking_response', array $serialized, array $row )

Parameters

$serialized array<string,mixed>
The camelCase payload the screen reads, such as id, status, startUtc, customerName, priceCents and paymentStatus.
$row array<string,mixed>
The raw database row, cancel_token included.

Return value

array<string,mixed> The payload. Add keys; keep the ones the screen reads.

Example

add_filter(
    'happenboard_rest_booking_response',
    function ( $item, $row ) {
        // Loaded for the whole page in happenboard_rest_bookings_prefetch.
        $id             = (int) $row['booking_id'];
        $item['source'] = (string) ( $GLOBALS['acme_booking_sources'][ $id ] ?? acme_source_for_booking( $id ) );

        return $item;
    },
    10,
    2
);

Notes

  • Two arguments: register with add_filter( ..., 10, 2 ).
  • It runs for each row of the Bookings screen and of the CSV export, and for the booking returned when an operator books for a customer. The CSV keeps its own columns: a key you add does not appear there.
  • To show the key as a column, add a DataViews field to window.happenBoard.bookingsExtraFields in an admin script loaded before the Bookings screen.
  • Never copy cancel_token from $row into the payload. HappenBoard Pro adds resourceName at priority 10.

happenboard_rest_bookings_prefetch

Action Free src/REST/Bookings.php

Fires once per page of the admin Bookings list, before happenboard_rest_booking_response filters each row. Load the data of your extra column here, in one query for the whole page.

Signature

do_action( 'happenboard_rest_bookings_prefetch', array $rows, WP_REST_Request $request )

Parameters

$rows array<int,array<string,mixed>>
The raw rows of the page, values as strings.
$request WP_REST_Request
The GET /happenboard/v1/bookings request, with its page and filters.

Example

add_action(
    'happenboard_rest_bookings_prefetch',
    function ( $rows ) {
        // One query for the whole page instead of one per row.
        $ids = array_map( 'intval', array_column( $rows, 'booking_id' ) );
        $GLOBALS['acme_booking_sources'] = acme_sources_for_bookings( $ids );
    }
);

Notes

  • Two arguments: register with add_action( ..., 10, 2 ) to receive the request.
  • Only the Bookings list fires it. The CSV export and an operator’s booking for a customer run the response filter without it, so that filter must cope with rows you did not load.
  • A page holds up to 200 rows (50 by default).

Next steps