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 happens | Hooks that fire |
|---|---|
| A Stripe ticket payment is confirmed | happenboard_rsvp_submitted for each seated line, then happenboard_stripe_checkout_completed |
| A paid line finds no seat left | happenboard_pro_ticket_unseated |
| A Stripe refund is made, in part or in full | happenboard_pro_stripe_refund_recorded |
| The refunds now cover the whole Stripe payment | happenboard_stripe_refunded, after the hook above |
| A WooCommerce order is paid | happenboard_pro_wc_order_fulfilled |
| A WooCommerce refund is saved | happenboard_pro_wc_order_refunded |
| A WooCommerce order is cancelled, refunded in full, failed or trashed | happenboard_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
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_idint - 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-typeandhappenboard-pro/update-ticket-typeabilities, 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
eventIdfrom the row, it may not be the event you were editing. - Sales, refunds and cancellations move the
soldandheldcounters without firing it. When Sell tickets through WooCommerce is on, Pro listens at priority 10 to update the linked product.
happenboard_pro_ticket_deleted
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_idint - The deleted ticket type. The row no longer exists.
-
$snapshotarray<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 thehappenboard-pro/delete-ticket-typeability, 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
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
-
$limitsarray{checkouts_per_hour:int,seats_per_hour:int} - Default
checkouts_per_hour10 andseats_per_hour60. -
$ipstring - The visitor’s address, as
happenboard_rsvp_client_ipreturned 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_houralso 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_ipfirst. When no valid address is found, no limit applies.
happenboard_stripe_checkout_completed
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_idint - 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_idsarray<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.
-
$sessionarray<string,mixed> - The Stripe Checkout Session as a plain array:
id,amount_total,currency(lowercase, such aseur),payment_intent,metadataand 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 oncheckout.session.async_payment_succeededfor 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_submittedfor each seated line, so their confirmation emails are already triggered. A line with no seat left fireshappenboard_pro_ticket_unseatedinstead. Pro listens at priority 10 to add apayment.receivedentry to each attendee’s activity.
happenboard_pro_ticket_unseated
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_idint - The attendee row written for the paid line: status
cancelledwhen Stripe refunded it automatically,waitlistotherwise. -
$contextarray<string,mixed> - Always
event_id,ticket_id,quantity,email,display_name,source(stripeorwoocommerce) andoutcome. From Stripe,outcomeisrefunded,refund_failedornot_refunded, withrefund_id,amount(minor units, or null when the line could not be priced),currency,payment_intentanderror. From WooCommerce,outcomeis alwaysnot_refunded, withorder_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 fromsrc/Service/Stripe/WebhookHandler.phpfor Stripe and fromsrc/Service/WooCommerce/OrderBridge.phpfor 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_submitteddoes not fire for these rows, but their IDs are in the lists passed tohappenboard_stripe_checkout_completedandhappenboard_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
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_intentstring - The Stripe PaymentIntent ID of the order. Every attendee row of the order stores it.
-
$gainedarray<int,int> - Attendee ID => amount newly refunded on that line, in minor units. Only lines whose refunded amount went up are listed.
-
$currencystring - 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_cancelledfired, 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.refundedwebhook that follows finds nothing new. Refunds of booking payments never fire it. Pro listens at priority 10 to add apayment.refundedentry to each attendee’s activity.
happenboard_stripe_refunded
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_intentstring - The Stripe PaymentIntent ID of the order.
-
$cancelledarray<int,int> - Attendee IDs cancelled by this last refund. Rows cancelled earlier are not listed, so the list can be empty.
-
$sourcearray<string,mixed> - What reported the refund, as an array. From the Stripe webhook, the Stripe Charge (
amount,amount_refunded,currencyand the rest). From the Orders screen, a short array:id(the refund ID),object(refund),amount,amount_refunded,currencyandpayment_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_refundedis the total refunded so far andcurrencyis 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_cancelledfired 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
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
-
$methodsarray<int,string> - Payment gateway IDs. Default
array( 'cod' )(Cash on delivery). -
$orderWC_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, 2if you need the order. - Why it exists: WooCommerce puts Cash on delivery orders straight into
processingwith 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
completedyet: acompletedorder always gets its tickets. Removingcodfrom the list issues Cash on delivery tickets as soon as the order isprocessing. - 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
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_idint - WooCommerce order ID. Its attendee rows store
wc_order_{id}as their payment reference. -
$createdarray<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.
-
$orderWC_Order - The WooCommerce order, already saved with the
_happenboard_fulfilledand_happenboard_attendee_idsmeta. Callsave()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
processingorcompletedand is paid (seehappenboard_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. $createdcan be empty and the hook still fires. A line that found no seat gets awaitlistrow, an order note andhappenboard_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.receivedentry to each attendee’s activity.
happenboard_pro_wc_order_refunded
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_idint - WooCommerce order ID.
-
$gainedarray<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.
-
$currencystring - 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 (seehappenboard_pro_wc_order_reversed). Pro listens at priority 10 to add apayment.refundedentry to each attendee’s activity.
happenboard_pro_wc_order_reversed
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_idint - WooCommerce order ID.
-
$cancelledarray<int,int> - Attendee IDs cancelled by this pass. Rows already cancelled are not listed.
-
$orderWC_Order - The WooCommerce order, already saved with the
_happenboard_reversedmeta.
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_cancelledhas already fired for each cancelled row. - It fires once per order, when a fulfilled order moves to
refunded,cancelledorfailed, 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_refundedfires 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
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
-
$hoursint - 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_holdsWP-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
- Refunding an order: how full and partial refunds are made from the Orders screen.
- Attendees and sign-up hooks: the registration and cancellation hooks these payments trigger.
- Appointment booking hooks: booking prices and payments.