Ticketing and payment hooks

Available in:Pro

These hooks follow the paid side of HappenBoard Pro: ticket types, Stripe and WooCommerce orders, and refunds. They also cover paid lines that found no seat, the limit on paid checkouts and the card hold on booking requests. Use them to copy sales and refunds into your accounting, CRM or other tools.

The site owner’s side is in Setting up Stripe and Refunding an order. WooCommerce sales are covered in Selling tickets through WooCommerce. If you are new to HappenBoard hooks, start with Extending HappenBoard with hooks.

Every hook on this page is fired by HappenBoard Pro. See What Pro adds.

How an order is recorded

When a payment is confirmed, Pro writes one attendee row per ticket line, with a sale row beside it. The sale row keeps what the line was charged, what has been refunded of it and whether its seats count against the ticket’s capacity. The Orders dashboard reads these amounts, so a later price change does not rewrite past orders.

Amounts are in minor units of the currency: cents for EUR, yen for JPY. A checkout is paid in one currency. A cart that mixes currencies is refused before Stripe is called, and nothing on this page fires.

A cart of free tickets from the Ticket Picker block never reaches Stripe. Its tickets are issued at once, and only happenboard_rsvp_submitted fires, with source set to free_ticket. See Attendees and sign-up hooks.

Which hook fires when

What happensHooks that fire
A Stripe ticket payment is confirmedhappenboard_rsvp_submitted for each seated line, then happenboard_stripe_checkout_completed
A paid line finds no seat lefthappenboard_pro_ticket_unseated
A Stripe refund is made, in part or in fullhappenboard_pro_stripe_refund_recorded
The refunds now cover the whole Stripe paymenthappenboard_stripe_refunded, after the hook above
A WooCommerce order is paidhappenboard_pro_wc_order_fulfilled
A WooCommerce refund is savedhappenboard_pro_wc_order_refunded
A WooCommerce order is cancelled, refunded in full, failed or trashedhappenboard_pro_wc_order_reversed

A Stripe refund can come from the Orders screen or from the Stripe dashboard: both are recorded the same way, and only once. Each attendee cancelled along the way also fires happenboard_rsvp_cancelled.

Booking payments

Pro takes appointment payments by answering the free plugin’s booking payment filters, described in Appointment booking hooks. Its only hook of its own for them is happenboard_pro_booking_hold_hours, at the end of this page. Booking payments never fire the Stripe hooks above.

happenboard_pro_ticket_saved

Action Pro src/REST/Tickets.php

Fires after a ticket type is created or updated. Only the ticket ID is passed: read the row again if you need its fields.

Signature

do_action( 'happenboard_pro_ticket_saved', int $ticket_id )

Parameters

$ticket_id int
The ticket type that was written. The row exists and is up to date.

Example

add_action(
    'happenboard_pro_ticket_saved',
    function ( $ticket_id ) {
        $tickets = new \HappenBoard\Pro\Service\Tickets\Repository();
        $ticket  = $tickets->find( (int) $ticket_id );
        if ( null === $ticket ) {
            return;
        }
        // $ticket['eventId'], $ticket['priceCents'], $ticket['currency']...
        my_crm_sync_ticket( $ticket );
    }
);

Notes

  • It fires from the Tickets panel and the tickets REST routes, from the happenboard-pro/create-ticket-type and happenboard-pro/update-ticket-type abilities, and for each ticket moved to another translation when the translation holding them is deleted for good. Nothing tells a create from an update.
  • A request with a bad value is refused with a 400 before anything is written, so the hook does not fire. Examples: a negative price, an unknown currency code, a sale date that cannot be read.
  • With WPML or Polylang, all translations of an event sell the same tickets, kept on one of them. A ticket added from another translation joins that set: read eventId from the row, it may not be the event you were editing.
  • Sales, refunds and cancellations move the sold and held counters without firing it. When Sell tickets through WooCommerce is on, Pro listens at priority 10 to update the linked product.

happenboard_pro_ticket_deleted

Action Pro src/REST/Tickets.php

Fires after a ticket type is deleted, with a copy of the row as it was just before. Use it to archive the ticket or remove what you built on it.

Signature

do_action( 'happenboard_pro_ticket_deleted', int $ticket_id, array<string,mixed>|null $snapshot )

Parameters

$ticket_id int
The deleted ticket type. The row no longer exists.
$snapshot array<string,mixed>|null
The ticket row read before the delete, with the keys of the tickets REST API: id, eventId, name, description, priceCents, currency, capacity, sold, held, saleStartUtc, saleEndUtc, sortOrder, metadata, createdUtc, updatedUtc. Every caller reads it first, so it is an array in practice.

Example

add_action(
    'happenboard_pro_ticket_deleted',
    function ( $ticket_id, $snapshot ) {
        if ( ! is_array( $snapshot ) ) {
            return;
        }
        // The row as it was just before the delete.
        error_log( sprintf(
            'Ticket %d (%s) removed from event %d, %d sold.',
            $ticket_id,
            $snapshot['name'],
            $snapshot['eventId'],
            $snapshot['sold']
        ) );
    },
    10,
    2
);

Notes

  • Two arguments: register with 10, 2, or you lose the snapshot, the only copy of the deleted row.
  • It fires from the Tickets panel (DELETE /happenboard/v1/tickets/{id}), from the happenboard-pro/delete-ticket-type ability, and for each ticket of an event deleted for good. Moving an event to the trash deletes nothing.
  • Deleting a ticket type removes its per-date counters but keeps its attendees and sale rows, so past orders keep their amounts. Deleting the event also deletes each ticket’s seat holds and sale rows before this hook fires, then the event’s attendees and check-ins.
  • When Sell tickets through WooCommerce is on, Pro moves the linked product to the trash at priority 10, using the snapshot. Do not delete that product yourself.

happenboard_pro_checkout_limits

Filter Pro src/REST/Checkout.php

Filters how many paid checkouts one IP address may start per hour, and how many seats they may hold. A paid checkout holds its seats for about 30 minutes while the buyer pays, so this limit stops a script from holding a whole event without paying.

Signature

apply_filters( 'happenboard_pro_checkout_limits', array $limits, string $ip )

Parameters

$limits array{checkouts_per_hour:int,seats_per_hour:int}
Default checkouts_per_hour 10 and seats_per_hour 60.
$ip string
The visitor’s address, as happenboard_rsvp_client_ip returned it.

Return value

array The two limits. A value below 1 is raised to 1, and a missing key keeps its default.

Example

add_filter(
    'happenboard_pro_checkout_limits',
    function ( $limits, $ip ) {
        // The box office computer sells many tickets an hour.
        if ( '203.0.113.10' === $ip ) {
            $limits['checkouts_per_hour'] = 200;
            $limits['seats_per_hour']     = 1000;
        }

        return $limits;
    },
    10,
    2
);

Notes

  • Paid carts only. A free cart follows the sign-up limits of the free plugin: 5 per address per minute and 3 per email address per hour.
  • seats_per_hour also caps a single checkout: a bigger cart is refused with a 400 asking the buyer to contact the organizer. Past either limit, the visitor gets a 429 asking them to wait.
  • A checkout is counted once the cart has passed its other checks, just before its seats are held, even when no seats are left. The count is kept per address and expires one hour after the last counted checkout.
  • Behind a proxy or Cloudflare, every visitor may share one address: fix it with happenboard_rsvp_client_ip first. When no valid address is found, no limit applies.

happenboard_stripe_checkout_completed

Action Pro src/Service/Stripe/WebhookHandler.php

Fires when a Stripe Checkout payment for event tickets is confirmed and its ticket lines have become attendees. This is the place to record a ticket sale.

Signature

do_action( 'happenboard_stripe_checkout_completed', int $event_id, array<int,int> $created_attendee_ids, array<string,mixed> $session )

Parameters

$event_id int
The event the buyer booked on, from the session metadata happenboard_event_id. On a translated site, this is the translation whose page was used, which may not be the one that holds the tickets.
$created_attendee_ids array<int,int>
Attendee IDs written by this delivery, one per ticket line, including lines that found no seat. Empty when a retried delivery found every line already written.
$session array<string,mixed>
The Stripe Checkout Session as a plain array: id, amount_total, currency (lowercase, such as eur), payment_intent, metadata and the rest.

Example

add_action(
    'happenboard_stripe_checkout_completed',
    function ( $event_id, $attendee_ids, $session ) {
        $session_id = (string) ( $session['id'] ?? '' );
        if ( '' === $session_id || get_option( 'my_sale_' . $session_id ) ) {
            return; // Already recorded by an earlier delivery.
        }
        my_accounting_record_sale(
            (int) $event_id,
            $attendee_ids,
            (int) ( $session['amount_total'] ?? 0 ),
            strtoupper( (string) ( $session['currency'] ?? '' ) )
        );
        update_option( 'my_sale_' . $session_id, 1, false );
    },
    10,
    3
);

Notes

  • Three arguments: register with 10, 3. The third one is an array, not a Stripe object.
  • It fires on checkout.session.completed, or on checkout.session.async_payment_succeeded for payment methods that settle later. Ticket payments only: booking payments and free tickets never fire it.
  • Stripe may deliver the same event again. A retry skips the lines already written and fires the hook again with only the new rows, possibly none. Key your work on $session['id'], as in the example.
  • It runs after happenboard_rsvp_submitted for each seated line, so their confirmation emails are already triggered. A line with no seat left fires happenboard_pro_ticket_unseated instead. Pro listens at priority 10 to add a payment.received entry to each attendee’s activity.

happenboard_pro_ticket_unseated

Action Pro src/Service/Stripe/WebhookHandler.php

Fires when a paid ticket line could not get a seat, because the ticket sold out before the payment arrived. The attendee row is written, but no confirmation email is sent. Use it to alert someone who can refund the buyer or find them a seat.

Signature

do_action( 'happenboard_pro_ticket_unseated', int $attendee_id, array<string,mixed> $context )

Parameters

$attendee_id int
The attendee row written for the paid line: status cancelled when Stripe refunded it automatically, waitlist otherwise.
$context array<string,mixed>
Always event_id, ticket_id, quantity, email, display_name, source (stripe or woocommerce) and outcome. From Stripe, outcome is refunded, refund_failed or not_refunded, with refund_id, amount (minor units, or null when the line could not be priced), currency, payment_intent and error. From WooCommerce, outcome is always not_refunded, with order_id.

Example

add_action(
    'happenboard_pro_ticket_unseated',
    function ( $attendee_id, $context ) {
        if ( 'refunded' === $context['outcome'] ) {
            return; // Stripe already gave the money back.
        }
        wp_mail(
            get_option( 'admin_email' ),
            'Paid ticket without a seat',
            sprintf(
                'Attendee #%d (%s) paid for %d seat(s) of ticket %d and got none. Outcome: %s.',
                $attendee_id,
                $context['email'],
                $context['quantity'],
                $context['ticket_id'],
                $context['outcome']
            )
        );
    },
    10,
    2
);

Notes

  • Two arguments: register with 10, 2. It fires from src/Service/Stripe/WebhookHandler.php for Stripe and from src/Service/WooCommerce/OrderBridge.php for WooCommerce.
  • Stripe: when a secret key is saved, Pro refunds the line before writing the row, and the row is cancelled. Without a key, when the refund fails, or for an old session with several lines and no unit price, nobody is refunded and the row goes on the waitlist.
  • WooCommerce never refunds automatically, since the refund must go through the gateway that took the money. The row goes on the waitlist and an order note asks the shop manager to refund the line or give a seat.
  • happenboard_rsvp_submitted does not fire for these rows, but their IDs are in the lists passed to happenboard_stripe_checkout_completed and happenboard_pro_wc_order_fulfilled. A sale row is still recorded with no seats counted. Pro listens at priority 10 to note it in the attendee’s activity.

happenboard_pro_stripe_refund_recorded

Action Pro src/Service/Orders/Refunds.php

Fires each time a Stripe refund is recorded on a ticket order, with the amount each attendee’s line gained. It covers full and partial refunds made from the Orders screen and refunds made in the Stripe dashboard. Use it to record refunds in your accounting.

Signature

do_action( 'happenboard_pro_stripe_refund_recorded', string $payment_intent, array<int,int> $gained, string $currency )

Parameters

$payment_intent string
The Stripe PaymentIntent ID of the order. Every attendee row of the order stores it.
$gained array<int,int>
Attendee ID => amount newly refunded on that line, in minor units. Only lines whose refunded amount went up are listed.
$currency string
ISO 4217 code of the order, in capitals (for example EUR).

Example

add_action(
    'happenboard_pro_stripe_refund_recorded',
    function ( $payment_intent, $gained, $currency ) {
        // One accounting line per attendee the refund reached.
        foreach ( $gained as $attendee_id => $minor_units ) {
            my_accounting_record_refund(
                $payment_intent,
                (int) $attendee_id,
                (int) $minor_units,
                $currency
            );
        }
    },
    10,
    3
);

Notes

  • Three arguments: register with 10, 3.
  • How a refund is shared: first over the lines cancelled with it, up to what is left on each, then over the other lines in proportion to what is left of them. A line never gets back more than it cost.
  • Only the lines chosen on the Orders screen and the lines a refund covers in full are cancelled, with their seats released and happenboard_rsvp_cancelled fired, before this hook runs. A partial refund from the Stripe dashboard cancels nobody unless it covers a whole line.
  • Each refund is recorded once: the Orders screen records its refund as soon as Stripe accepts it, and the charge.refunded webhook that follows finds nothing new. Refunds of booking payments never fire it. Pro listens at priority 10 to add a payment.refunded entry to each attendee’s activity.

happenboard_stripe_refunded

Action Pro src/Service/Orders/Refunds.php

Fires once a Stripe ticket payment has been refunded in full, after its remaining attendees have been cancelled and their seats released. Use it to close the whole sale in your own systems.

Signature

do_action( 'happenboard_stripe_refunded', string $payment_intent, array<int,int> $cancelled, array<string,mixed> $source )

Parameters

$payment_intent string
The Stripe PaymentIntent ID of the order.
$cancelled array<int,int>
Attendee IDs cancelled by this last refund. Rows cancelled earlier are not listed, so the list can be empty.
$source array<string,mixed>
What reported the refund, as an array. From the Stripe webhook, the Stripe Charge (amount, amount_refunded, currency and the rest). From the Orders screen, a short array: id (the refund ID), object (refund), amount, amount_refunded, currency and payment_intent.

Example

add_action(
    'happenboard_stripe_refunded',
    function ( $payment_intent, $cancelled, $source ) {
        // The whole payment is refunded: close the sale.
        my_accounting_close_sale(
            $payment_intent,
            (int) ( $source['amount_refunded'] ?? 0 ),
            strtoupper( (string) ( $source['currency'] ?? '' ) ),
            $cancelled
        );
    },
    10,
    3
);

Notes

  • Three arguments: register with 10, 3. The third one is a Charge only when the refund came through the webhook: check $source['object'] before reading other keys. amount_refunded is the total refunded so far and currency is lowercase in both cases.
  • Partial refunds do not fire it until they add up to the whole payment. To follow each refund, use happenboard_pro_stripe_refund_recorded, which fires just before this hook when the refund brought new money back.
  • On a full refund, every line of the payment is marked refunded in full and every line not yet cancelled is cancelled, with happenboard_rsvp_cancelled fired for each before this hook.
  • Make it safe to run twice for one payment. A later webhook can fire it again when it cancels a line the Orders screen left, such as an old line with no recorded amount. It never fires for booking payments. Pro no longer listens here: the attendee activity comes from happenboard_pro_stripe_refund_recorded.

happenboard_pro_wc_pay_later_methods

Filter Pro src/Service/WooCommerce/OrderBridge.php

Filters the WooCommerce payment methods whose orders reach processing before any money is collected. Tickets for those orders are issued only when the order is marked completed.

Signature

apply_filters( 'happenboard_pro_wc_pay_later_methods', array<int,string> $methods, WC_Order $order )

Parameters

$methods array<int,string>
Payment gateway IDs. Default array( 'cod' ) (Cash on delivery).
$order WC_Order
The order being checked before its tickets are issued.

Return value

array<int,string> The gateway IDs to treat as pay later, compared strictly with $order->get_payment_method().

Example

add_filter(
    'happenboard_pro_wc_pay_later_methods',
    function ( $methods, $order ) {
        // This gateway puts orders in "processing" before any money is in.
        $methods[] = 'my_pay_on_site';

        return $methods;
    },
    10,
    2
);

Notes

  • Two arguments: register with 10, 2 if you need the order.
  • Why it exists: WooCommerce puts Cash on delivery orders straight into processing with nothing collected, so the status alone does not tell them from a paid order.
  • It is only asked about a paid order that is not completed yet: a completed order always gets its tickets. Removing cod from the list issues Cash on delivery tickets as soon as the order is processing.
  • It needs WooCommerce active and Sell tickets through WooCommerce on. It can run several times for one order, once per status change, so keep it free of side effects.

happenboard_pro_wc_order_fulfilled

Action Pro src/Service/WooCommerce/OrderBridge.php

Fires once a paid WooCommerce order has been turned into HappenBoard attendees. Use it to push the order to invoicing or CRM tools now that attendee IDs exist.

Signature

do_action( 'happenboard_pro_wc_order_fulfilled', int $order_id, array<int,int> $created, WC_Order $order )

Parameters

$order_id int
WooCommerce order ID. Its attendee rows store wc_order_{id} as their payment reference.
$created array<int,int>
Attendee IDs created by this pass, in line order, including lines that found no seat. Empty when the order had no ticket products.
$order WC_Order
The WooCommerce order, already saved with the _happenboard_fulfilled and _happenboard_attendee_ids meta. Call save() yourself if you change it.

Example

add_action(
    'happenboard_pro_wc_order_fulfilled',
    function ( $order_id, $created, $order ) {
        if ( array() === $created ) {
            return; // No ticket lines on this order.
        }
        $order->add_order_note( sprintf(
            '%d HappenBoard attendee row(s) created.',
            count( $created )
        ) );
        $order->save();
    },
    10,
    3
);

Notes

  • Three arguments: register with 10, 3. It needs WooCommerce active and Sell tickets through WooCommerce on.
  • It fires when the order reaches processing or completed and is paid (see happenboard_pro_wc_pay_later_methods), once per order. A run that stopped midway is finished by the next status change, which skips the lines already written. Nothing fires when the order has no valid billing email.
  • $created can be empty and the hook still fires. A line that found no seat gets a waitlist row, an order note and happenboard_pro_ticket_unseated, but no confirmation email.
  • Each line writes a sale row with what the buyer paid for it, discounts and tax included, in minor units. Pro listens at priority 10 to add a payment.received entry to each attendee’s activity.

happenboard_pro_wc_order_refunded

Action Pro src/Service/WooCommerce/OrderBridge.php

Fires when a WooCommerce refund reaches the ticket sales of a fulfilled order, with what each attendee’s sale gained. Use it to record refunds in your accounting. It moves money only: a refund alone cancels no attendee.

Signature

do_action( 'happenboard_pro_wc_order_refunded', int $order_id, array<int,int> $gained, string $currency )

Parameters

$order_id int
WooCommerce order ID.
$gained array<int,int>
Attendee ID => amount newly refunded on that attendee’s sale, in minor units of the order currency. Only sales whose refunded amount went up are listed.
$currency string
ISO 4217 code of the order currency, as WooCommerce stores it (for example EUR).

Example

add_action(
    'happenboard_pro_wc_order_refunded',
    function ( $order_id, $gained, $currency ) {
        // Only the amount new since the last call.
        foreach ( $gained as $attendee_id => $minor_units ) {
            my_accounting_record_refund(
                'wc_' . (int) $order_id,
                (int) $attendee_id,
                (int) $minor_units,
                $currency
            );
        }
    },
    10,
    3
);

Notes

  • Three arguments: register with 10, 3.
  • It is checked when WooCommerce saves a refund, when a refund is deleted and when an order is reversed. Amounts are recomputed from the order each time, so a refund seen twice is counted once. A deleted refund lowers the amounts without firing the hook.
  • How a refund is split: a full refund covers every sale. A refund that names lines on the WooCommerce refund screen goes to the attendee each line produced. An amount that names no line is shared by each sale’s part of the order total. No sale gets back more than it cost.
  • A full refund also moves the order to refunded, which reverses it (see happenboard_pro_wc_order_reversed). Pro listens at priority 10 to add a payment.refunded entry to each attendee’s activity.

happenboard_pro_wc_order_reversed

Action Pro src/Service/WooCommerce/OrderBridge.php

Fires after a fulfilled WooCommerce order has been called off, once its attendees are cancelled and their seats released. Use it to undo what you did on fulfilment.

Signature

do_action( 'happenboard_pro_wc_order_reversed', int $order_id, array<int,int> $cancelled, WC_Order $order )

Parameters

$order_id int
WooCommerce order ID.
$cancelled array<int,int>
Attendee IDs cancelled by this pass. Rows already cancelled are not listed.
$order WC_Order
The WooCommerce order, already saved with the _happenboard_reversed meta.

Example

add_action(
    'happenboard_pro_wc_order_reversed',
    function ( $order_id, $cancelled, $order ) {
        // Undo what you did when the order was fulfilled.
        foreach ( $cancelled as $attendee_id ) {
            my_crm_mark_cancelled( (int) $attendee_id, (int) $order_id );
        }
    },
    10,
    3
);

Notes

  • Three arguments: register with 10, 3. happenboard_rsvp_cancelled has already fired for each cancelled row.
  • It fires once per order, when a fulfilled order moves to refunded, cancelled or failed, or is trashed or deleted. When another request is fulfilling the same order at that moment, the reversal is retried a minute later from WP-Cron.
  • Calling an order off is not refunding it. A cancelled order whose money the shop kept stays counted as revenue: sales are marked refunded only for what WooCommerce has refunded, and happenboard_pro_wc_order_refunded fires first when there is anything new.
  • Seats are released only for rows that hold them, and the linked products’ stock is refreshed. It needs WooCommerce active and Sell tickets through WooCommerce on.

happenboard_pro_booking_hold_hours

Filter Pro src/Service/Booking/AuthorizationSweeper.php

Filters how long a booking request may wait for your approval while the customer’s card is held, in hours. An hourly check declines older requests, which releases the card hold before Stripe cancels it on its own.

Signature

apply_filters( 'happenboard_pro_booking_hold_hours', int $hours )

Parameters

$hours int
Default 144 (six days).

Return value

int How many hours a request with a held card may wait. A value below 1 counts as 1.

Example

add_filter(
    'happenboard_pro_booking_hold_hours',
    function ( $hours ) {
        // Decline unanswered requests after four days instead of six.
        return 96;
    }
);

Notes

  • Keep it under 168 hours. Stripe cancels an uncaptured card authorization after seven days; after that, approving the request finds nothing to charge.
  • The age is counted from when the booking was made, not from the card authorization a few minutes later, so the check errs on the early side.
  • The check runs hourly on the happenboard_pro_expire_booking_holds WP-Cron event and reads the filter once per run. A declined request goes through the normal decline: the customer is told, the slot is freed, the card hold is released and a note explains why. The site admin email gets one summary per run.
  • Not to be confused with the free plugin’s happenboard_booking_payment_hold_hours, which sets how long an unpaid booking keeps its slot.

Next steps