HappenBoard fires a set of actions and filters around attendee registration, so you can add your own fields to the sign-up form, react to a new registration, or change how a request is identified.
Two of them work as a pair: happenboard_rsvp_form_fields renders an extra input, and happenboard_rsvp_custom_fields decides whether what the visitor typed is kept. Rendering a field without validating it stores nothing.
happenboard_attendee_deleted
Fires once an attendee row has been permanently deleted through the admin, at the end of DELETE /attendees/{id}. Use it to drop whatever you store about that attendee on your side.
Signature
do_action( 'happenboard_attendee_deleted', int $attendee_id )
Parameters
-
$attendee_idint - Primary key of the row that was just deleted.
Example
add_action(
'happenboard_attendee_deleted',
function ( int $attendee_id ): void {
global $wpdb;
$wpdb->delete(
$wpdb->prefix . 'my_plugin_badges',
array( 'attendee_id' => $attendee_id ),
array( '%d' )
);
}
);
Notes
- A single argument is passed, so a plain
add_action()with no priority or accepted-args count is enough. - The row is already gone when the action runs: the delete query executes first, so looking the id up in the attendees table returns nothing. Capture what you need earlier, for instance on
happenboard_attendee_status_changed, or keep your own copy. - Only the REST delete route fires this. Cancelling a sign-up does not delete anything: it fires
happenboard_rsvp_cancelled, and an admin status change fireshappenboard_attendee_status_changed. - HappenBoard Pro already listens, at priority 10 to wipe the attendee timeline and at priority 20 to emit the
attendee.deletedwebhook. Your own callback does not disturb either. - The call has no docblock in the source. The argument documented here comes from reading the
do_action()call itself.
happenboard_attendee_status_changed
Fires when an administrator changes an attendee’s status through PATCH /attendees/{id}, for example moving someone from the waitlist to going, or marking them cancelled from the attendees list.
Signature
do_action( 'happenboard_attendee_status_changed', int $attendee_id, string $new_status, string $old_status )
Parameters
-
$attendee_idint - Primary key of the attendee row.
-
$new_statusstring - The status that was just written, taken from the request.
-
$old_statusstring - The status read from the row before the update. Empty string when the row had none.
Example
add_action(
'happenboard_attendee_status_changed',
function ( int $attendee_id, string $new_status, string $old_status ): void {
if ( 'going' === $new_status && 'waitlist' === $old_status ) {
error_log( sprintf( 'Attendee %d promoted from the waitlist.', $attendee_id ) );
}
},
10,
3
);
Notes
- Three arguments are passed, so register with
10, 3. With the default of one you only get the id. - The new status comes second and the old one third. Both are plain strings, so swapping them produces silently wrong results rather than an error.
- It fires after the database write, and only when that write succeeded. A failed update returns a 500 from the endpoint and the action never runs.
- Setting the status to
cancelledfrom the admin fires this hook, nothappenboard_rsvp_cancelled. Listen to both if you want every cancellation, whoever triggered it. - The call has no docblock. HappenBoard Pro consumes it with three arguments in both its timeline listener (priority 10) and its webhook catalogue (priority 20), which confirms the order.
happenboard_attendee_updated
Fires when an administrator edits an attendee’s profile fields (display name, email or quantity) through PATCH /attendees/{id}. The payload is a before and after diff limited to the fields that really changed.
Signature
do_action( 'happenboard_attendee_updated', int $attendee_id, array<string,array{from:mixed,to:mixed}> $changes )
Parameters
-
$attendee_idint - Primary key of the attendee row.
-
$changesarray<string,array{from:mixed,to:mixed}> - Diff keyed by database column, each entry holding
fromandto.
Example
add_action(
'happenboard_attendee_updated',
function ( int $attendee_id, array $changes ): void {
if ( ! isset( $changes['email'] ) ) {
return;
}
my_plugin_sync_contact(
(string) $changes['email']['from'],
(string) $changes['email']['to']
);
},
10,
2
);
Notes
- Two arguments are passed, so register with
10, 2. - The keys of
$changesare database column names:display_name,emailandquantity. They are not the camelCase names the REST request uses. - Only fields whose value actually changed appear. A request that resends identical values still fires the action, with an empty array. Return early on an empty diff.
- Status is not part of this diff. A single request that edits the profile and flips the status fires this hook and
happenboard_attendee_status_changed, profile first. - The call has no docblock. HappenBoard Pro reads the diff with two arguments to render the before and after in the attendee timeline.
happenboard_pro_attendee_checked_in
Fires after a check-in scan has been recorded at the door. Use it to print a badge, notify a team channel, or refresh a live roster.
Signature
do_action( 'happenboard_pro_attendee_checked_in', int $checkin_id, int $attendee_id, int $event_id )
Parameters
-
$checkin_idint - Primary key of the check-in row that was just inserted.
-
$attendee_idint - The attendee who was scanned.
-
$event_idint - The event the attendee was scanned into.
Example
add_action(
'happenboard_pro_attendee_checked_in',
function ( int $checkin_id, int $attendee_id, int $event_id ): void {
error_log(
sprintf(
'Scan %d: attendee %d entered event %d.',
$checkin_id,
$attendee_id,
$event_id
)
);
},
10,
3
);
Notes
- Three arguments are passed, so register with
10, 3. - The first argument is the check-in row id, not the attendee id. It is easy to use the wrong one because both are integers.
- It fires on every scan, re-scans included. A second scan of the same ticket inserts a second row and fires again with a new
$checkin_id. Deduplicate yourself if you only want the first entry. - It only fires after a successful insert. When the insert fails the repository returns 0 and nothing is dispatched.
- HappenBoard Pro already listens: the attendee timeline at 10, the MailerPress bridge and the webhook dispatcher at 20, Slack at 30. The MailerPress bridge re-emits it as
happenboard_mp_checked_in.
happenboard_rest_attendee_response
Filters one serialized attendee row on its way out of the REST API. This is how you add your own columns to the admin attendee list.
Signature
apply_filters( 'happenboard_rest_attendee_response', array<string,mixed> $serialized, array<string,mixed> $row )
Parameters
-
$serializedarray<string,mixed> - The camelCase payload sent to the admin interface:
id,eventId,email,displayName,status,quantity,customFieldsand the timestamps. -
$rowarray<string,mixed> - The raw database row, with snake_case columns and
custom_fieldsstill JSON encoded.
Return value
array<string,mixed>
The serialized attendee, with your own keys added. Always return an array.
Example
add_filter(
'happenboard_rest_attendee_response',
function ( array $serialized, array $row ): array {
$serialized['myPluginTier'] = (string) get_post_meta(
(int) ( $row['event_id'] ?? 0 ),
'_my_plugin_tier',
true
);
return $serialized;
},
10,
2
);
Notes
- Two arguments are passed, so register with
10, 2. The raw row is there so you do not have to query the table again. - The result is cast with
(array). Returning a string, an object ornullproduces a broken row instead of an error, so always return the array you were handed. - The same serializer feeds the list endpoint and the CSV or JSON export, so any column you add here also lands in exported files.
- It runs once per row. A query inside your callback becomes one query per attendee on a list of 200. HappenBoard Pro already pays that cost here to add
checkinCountandlastCheckinUtcat priority 10. - Add keys, do not rebuild the array. Replacing
$serializedwholesale drops both the core fields and anything an earlier callback added.
happenboard_rsvp_cancelled
Fires when an attendee’s sign-up is cancelled and their seat released, whether the visitor cancelled it themselves or a payment was refunded. Use it to free up your own resources or trigger a win-back message.
Signature
do_action( 'happenboard_rsvp_cancelled', int $attendee_id )
Parameters
-
$attendee_idint - Primary key of the attendee row that was cancelled.
Example
add_action(
'happenboard_rsvp_cancelled',
function ( int $attendee_id ): void {
$repository = new \HappenBoard\Service\Attendees\Repository();
$attendee = $repository->find_by_id( $attendee_id );
if ( null !== $attendee ) {
error_log( 'Seat released for ' . (string) $attendee['email'] );
}
}
);
Notes
- A single argument is passed. The row is cancelled, not deleted, so you can still read it from the attendees repository with its status set to
cancelled. - Four different places fire it. In HappenBoard: the REST cancel route in
src/REST/Rsvp.phpand the public cancel page insrc/Service/Frontend/CancelPage.php. In HappenBoard Pro: the Stripe refund handler insrc/Service/Stripe/WebhookHandler.phpand the WooCommerce order reversal insrc/Service/WooCommerce/OrderBridge.php. - Because of the Pro paths, your callback can run inside an incoming payment webhook with no logged-in user and no front-end context. Do not call anything that assumes a current user or a rendered page.
- A refund or a reversed order fires it once per attendee row created by that order, so a single refund can fire it several times in a row.
- An administrator setting the status to
cancelledfrom the attendees list does not fire this hook. That path fireshappenboard_attendee_status_changedinstead.
happenboard_rsvp_client_ip
Filters the client IP the sign-up rate limiter keys on. HappenBoard reads REMOTE_ADDR and deliberately ignores X-Forwarded-For, so behind a reverse proxy or a CDN every visitor shows up as the proxy address and five sign-ups per minute lock out the whole site.
Signature
apply_filters( 'happenboard_rsvp_client_ip', string $ip )
Parameters
-
$ipstring REMOTE_ADDRafterFILTER_VALIDATE_IP, or an empty string when it could not be parsed.
Return value
string
A validated IP address used as the rate-limiter key. Return $ip unchanged when you cannot do better.
Example
add_filter(
'happenboard_rsvp_client_ip',
function ( string $ip ): string {
// Only read this header if the site really is behind Cloudflare.
$forwarded = isset( $_SERVER['HTTP_CF_CONNECTING_IP'] )
? sanitize_text_field( wp_unslash( (string) $_SERVER['HTTP_CF_CONNECTING_IP'] ) )
: '';
return filter_var( $forwarded, FILTER_VALIDATE_IP ) ? $forwarded : $ip;
}
);
Notes
- The correct fix is to read the one header your own proxy sets, validate it with
filter_var( $value, FILTER_VALIDATE_IP ), and fall back to$ipwhen it does not parse. Never return a header value straight through: a header a visitor controls is a throttle a visitor can rotate away. - If you use
X-Forwarded-For, take the hop your own proxy appended, not the first entry of the list, which the client can write freely. When you do not know for sure what sits in front of WordPress, leave this filter alone. - One argument only, so a plain
add_filter()is enough. - The same filter is applied in HappenBoard Pro’s front-end event submission endpoint (
src/REST/EventSubmissions.php), so a single callback changes both throttles: 5 sign-ups per minute and 3 event submissions per 10 minutes. Appointment bookings have their own copy,happenboard_booking_client_ipinsrc/REST/Bookings.php: filtering one does not filter the other. - Returning an empty string switches the per-IP throttle off entirely on both endpoints, since both skip it when the address is empty. The per-email throttle, the honeypot and the minimum fill time still apply. The value is also passed to Turnstile and Akismet as the visitor IP, so a wrong value degrades spam scoring.
happenboard_rsvp_custom_fields
Validates the extra answers submitted with a sign-up and decides what gets stored on the attendee row. This is the companion of happenboard_rsvp_form_fields: that one draws the input, this one keeps the value.
Signature
apply_filters( 'happenboard_rsvp_custom_fields', array<string,mixed> $fields, array<string,string|array<int,string>> $raw, int $event_id )
Parameters
-
$fieldsarray<string,mixed> - The answers accumulated so far. An empty array by default.
-
$rawarray<string,string|array<int,string>> - What the visitor submitted, keyed by field key. Keys are already through
sanitize_key()and values throughsanitize_text_field(). -
$event_idint - The event being signed up for.
Return value
array<string,mixed>|WP_Error
The answers to store on the attendee row, or a WP_Error to reject the sign-up.
Example
add_filter(
'happenboard_rsvp_custom_fields',
function ( $fields, array $raw, int $event_id ) {
if ( ! is_array( $fields ) ) {
$fields = array();
}
$diet = isset( $raw['diet'] ) ? sanitize_text_field( (string) $raw['diet'] ) : '';
if ( '' === $diet ) {
return new WP_Error(
'my_plugin_diet_required',
__( 'Please tell us about your dietary requirements.', 'my-plugin' ),
array( 'status' => 400 )
);
}
$fields['diet'] = $diet;
return $fields;
},
20,
3
);
Notes
- Three arguments are passed, so register with
10, 3at least. Use priority20when HappenBoard Pro may be active: its own validator runs at 10. - Merge into the incoming array rather than replacing it. When Pro has attendee fields defined for that event, its validator returns only the values built from its own definitions, so anything a lower priority added is dropped. Running after it and merging is the safe order.
- The keys in
$raware input names with thehappenboard_cf_prefix removed by the form script, which also skips disabled inputs. An input not namedhappenboard_cf_<key>never reaches this filter, no matter how you rendered it. - Return a
WP_Errorto refuse the sign-up: it is handed straight back by the REST endpoint and its message is shown under the form. Anything that is neither an array nor aWP_Erroris turned into an empty array and every answer is dropped silently. - What you return is JSON encoded into the attendee’s
custom_fieldscolumn, so stick to scalars and arrays of strings. An empty array is stored asNULL. Values arriving pre-sanitized is not the same as validated: check ranges, formats and allowed options yourself.
happenboard_rsvp_form_fields
Injects extra HTML into the sign-up form, after the email field and before the quantity selector. This is the hook to use when you want your own inputs on the form.
Signature
apply_filters( 'happenboard_rsvp_form_fields', string $html, int $post_id )
Parameters
-
$htmlstring - The HTML to inject. Empty by default.
-
$post_idint - The ID of the event being registered for.
Return value
string
HTML printed as is. Escaping is your responsibility.
Example
add_filter(
'happenboard_rsvp_form_fields',
function ( string $html, int $post_id ): string {
$html .= sprintf(
'<label class="wp-block-happenboard-rsvp-form__field">
<span>%s</span>
<input type="text" name="happenboard_cf_diet" maxlength="190">
</label>',
esc_html__( 'Dietary requirements', 'my-plugin' )
);
return $html;
},
10,
2
);
Notes
- The filter passes two arguments, so pass
10, 2toadd_filter(). Leaving out the2means$post_idnever reaches your callback. - Append to
$htmlrather than replacing it. HappenBoard Pro renders its own custom fields through this same filter, and returning a fresh string would silently remove them. - The value is printed without escaping, so escape everything you build yourself.
- Rendering an input is not enough to store what the visitor types. The submitted value is discarded unless you also validate it through
happenboard_rsvp_custom_fields, which fires insrc/REST/Rsvp.phpwhen the form is posted. Naming your inputshappenboard_cf_<key>follows the convention HappenBoard Pro uses.
happenboard_rsvp_submitted
Fires right after an attendee row has been created, on free sign-ups as well as on paid orders. This is the entry point for everything that has to happen when someone registers.
Signature
do_action( 'happenboard_rsvp_submitted', int $attendee_id, array{event_id:int,email:string,display_name:string,quantity:int,cancel_token:string} $data )
Parameters
-
$attendee_idint - Primary key of the attendee row that was just inserted.
-
$dataarray<string,mixed> - The submission payload:
event_id,email,display_name,quantityandcancel_token.
Example
add_action(
'happenboard_rsvp_submitted',
function ( int $attendee_id, array $data ): void {
error_log(
sprintf(
'Attendee %d signed up for event %d with %d seat(s).',
$attendee_id,
(int) ( $data['event_id'] ?? 0 ),
(int) ( $data['quantity'] ?? 1 )
)
);
},
10,
2
);
Notes
- Two arguments are passed, so register with
10, 2. - Three places fire it with the same five keys: the sign-up endpoint in HappenBoard (
src/REST/Rsvp.php), and in HappenBoard Pro the Stripe checkout webhook (src/Service/Stripe/WebhookHandler.php) and the WooCommerce order bridge (src/Service/WooCommerce/OrderBridge.php). Paid paths fire once per ticket line, so a single order can fire it several times. $datais the submission payload, not the database row. It carries nostatus, nocustom_fieldsand no timestamps. Read the row through the attendees repository when you need those.cancel_tokenis the secret behind the one-click cancel link. It is deliberately kept out of the REST response. Do not log it, do not put it in a URL you expose, do not forward it to a third party.- On the two paid paths the token is read back with a query for the newest row of the event rather than by primary key, so under concurrent orders it can be another attendee’s token. Read the row by
$attendee_idif the token matters to you. The free confirmation email is sent on this hook at priority 10; HappenBoard Pro listens at 10, 20, 30 and 40.