These HappenBoard Pro hooks run where your site talks to other systems, and at the door. They cover outgoing webhooks and Slack, the Google and Outlook calendar sync, the check-in scanner and badge printing.
The setup side is in Outbound webhooks and QR code check-in. If you are new to hooks, start with Extending HappenBoard with hooks.
Verifying a webhook signature
Every webhook delivery is a POST with a JSON body holding event, delivered_at, delivery_id and data. It carries these headers:
X-HappenBoard-Event: the event key, such asrsvp.created.X-HappenBoard-Delivery: an ID that stays the same on every retry of one delivery.X-HappenBoard-Timestamp: the Unix time of this attempt.X-HappenBoard-Signature-V1:t=<timestamp>,v1=<hex>, an HMAC-SHA256 of the timestamp, a dot and the raw body, keyed with the webhook’s signing secret.X-HappenBoard-Signature:sha256=<hex>, an HMAC of the body alone. It is still sent for older receivers but gives no protection against replays.
Check the v1 signature in constant time, and refuse a timestamp more than five minutes from your clock. Changing t breaks the signature, so a captured request cannot be replayed later.
// $secret is the webhook's signing secret.
parse_str( str_replace( ',', '&', $_SERVER['HTTP_X_HAPPENBOARD_SIGNATURE_V1'] ?? '' ), $sig );
$t = (string) ( $sig['t'] ?? '' );
$ok = ctype_digit( $t )
&& abs( time() - (int) $t ) <= 300
&& hash_equals( hash_hmac( 'sha256', $t . '.' . file_get_contents( 'php://input' ), $secret ), (string) ( $sig['v1'] ?? '' ) );
Hash the body exactly as it arrived, before decoding the JSON. Then use X-HappenBoard-Delivery to ignore a delivery you already processed: each retry keeps the ID but gets a fresh timestamp.
The signing secret is shown once: when the webhook is created, and each time someone clicks Generate a new secret in its editor. After that, the webhook list only shows its last four characters. Over REST, POST /happenboard/v1/pro/webhooks answers the new secret once, and POST /happenboard/v1/pro/webhooks/{id}/secret replaces it.
How deliveries are retried and paused
Deliveries leave in a background job, never in the request that triggered them. A failure that looks temporary (no response, a timeout, HTTP 408, 429 or 5xx) is retried 1, 4, 16, 64 and 256 minutes later: six attempts over about six hours. Retries go through Action Scheduler when it is installed, WP-Cron otherwise. Any other answer is final, and redirects are never followed.
Each failed attempt adds one to the webhook’s count of failures in a row, and a 2xx answer sets it back to zero. At 50, Pro switches the webhook off, its waiting retries stop, the list shows it as Paused and the site admin gets an email. A webhook already past the limit is switched off when the Webhooks card lists it. Switching it back on starts the count again.
The Test button sends a webhook.test event at once. It fires the delivery hooks like a real attempt, but is never retried and never counted.
Check-in at the door
- Each scan calls
POST /happenboard/v1/pro/checkin/markwith the ticket’s code, plus the event picked in Event at this door when there is one. - A ticket code that fails verification gets HTTP 400 and counts towards the lockout (
happenboard_pro_checkin_lockout). - A genuine ticket is refused with HTTP 409 when it is for another event, when its booking holds no seat, or when it is for another date of a recurring event. That fires
happenboard_pro_attendee_checkin_refused. - An accepted scan fires
happenboard_pro_attendee_checked_in, documented with the attendee hooks. - The scanner’s Undo button deletes that check-in and fires
happenboard_pro_attendee_checkin_undone.
The filters that shape the event sent to Google and Outlook are documented with the event hooks.
happenboard_pro_webhook_delivered
Fires after each outgoing webhook attempt that gets an HTTP response back, whatever the status code. Check $status to tell a success from a refusal.
Signature
do_action( 'happenboard_pro_webhook_delivered', array $webhook, string $event, array $data, int $status, string $body )
Parameters
-
$webhookarray<string,mixed> - The webhook:
id,label,url,secret,events,active,createdUtc,updatedUtcandlastResult(utc,status,errorandfailuresof the previous attempt). It holds the signing secret: never log it whole. -
$eventstring - The event key, also sent in
X-HappenBoard-Event:rsvp.created,rsvp.cancelled,attendee.status_changed,attendee.checked_in,attendee.checkin_undone,attendee.deleted, orwebhook.testfor the test ping. -
$dataarray<string,mixed> - The payload sent in the body’s
datakey. An attendee’s name and email are read from their row for each attempt, so a person erased since the event is sent as the erasure left them. -
$statusint - HTTP status code of the answer. Only 200 to 299 counts as delivered.
-
$bodystring - The raw response body.
Example
add_action(
'happenboard_pro_webhook_delivered',
function ( $webhook, $event, $data, $status, $body ) {
if ( $status >= 200 && $status < 300 ) {
return; // Delivered.
}
// This hook also fires for 3xx, 4xx and 5xx answers.
error_log( sprintf(
'Webhook %s to %s answered HTTP %d: %s',
$event,
(string) ( $webhook['url'] ?? '' ),
$status,
substr( $body, 0, 200 )
) );
},
10,
5
);
Notes
- Five arguments are passed, so register with
10, 5. - Despite its name, it fires for every HTTP answer, error codes included, and once per attempt: a receiver answering 500 can fire it six times for one delivery.
- An attempt stopped before sending fires nothing: a URL that does not resolve to a public address, or a port other than 80, 443 and 8080. The core
http_allowed_safe_portsfilter can widen that port list. - Pro keeps only the latest outcome on the webhook, which the admin list shows. To keep a history of deliveries, log them here.
happenboard_pro_webhook_failed
Fires when an outgoing webhook attempt gets no HTTP response at all: a DNS failure, a refused connection, a TLS error or the 5-second timeout. It passes the error message.
Signature
do_action( 'happenboard_pro_webhook_failed', array $webhook, string $event, array $data, string $error )
Parameters
-
$webhookarray<string,mixed> - The webhook, with the same keys as in
happenboard_pro_webhook_delivered, signing secret included. -
$eventstring - The event key, for example
attendee.checked_in, orwebhook.testfor the test ping. -
$dataarray<string,mixed> - The payload that was being sent in the body’s
datakey. -
$errorstring - The message of the
WP_Errorreturned by the HTTP client, such as a cURL timeout.
Example
add_action(
'happenboard_pro_webhook_failed',
function ( $webhook, $event, $data, $error ) {
// Pro retries on its own: alert once, on the third failure in a row.
$failures = (int) ( $webhook['lastResult']['failures'] ?? 0 ) + 1;
if ( 3 !== $failures ) {
return;
}
wp_mail(
get_option( 'admin_email' ),
'Webhook unreachable',
sprintf( '%s (%s): %s', (string) ( $webhook['url'] ?? '' ), $event, $error )
);
},
10,
4
);
Notes
- Four arguments are passed, so register with
10, 4. - Pro already retries these failures and fires the hook on each attempt. Use it to alert or log, not to send again, or the receiver gets duplicates.
- A receiver that answers with an error code, 500 for example, fires
happenboard_pro_webhook_deliveredinstead. Listen to both to see every attempt. - For an endpoint that stays down,
happenboard_pro_webhook_auto_pausedgives you one alert when Pro gives up.
happenboard_pro_webhook_auto_pause_after
Filters how many failed delivery attempts in a row switch a webhook off. Return 0 to keep every webhook on, whatever happens.
Signature
apply_filters( 'happenboard_pro_webhook_auto_pause_after', int $attempts )
Parameters
-
$attemptsint - Default
50, a little over eight events’ worth of attempts at six attempts each.
Return value
int
Failed attempts in a row after which the webhook is switched off. A negative value counts as 0.
Example
add_filter(
'happenboard_pro_webhook_auto_pause_after',
function ( $attempts ) {
// Our CRM has long maintenance windows: give it more room.
return 200;
}
);
Notes
- Every failed attempt counts, retries included: no response, a non-2xx answer, or a URL refused before sending. A 2xx answer sets the count back to zero. Test pings are not counted.
- The limit is checked when a failure is recorded, and when the Webhooks card lists the webhooks. A webhook already over the limit, or over a lower value you just set, is switched off the next time the card is opened or a delivery fails.
- The webhook list reads it too, to tell a webhook switched off for failing (
autoPaused, shown as Paused) from one switched off by hand. - Switching the webhook back on, in the panel or with
PATCH /happenboard/v1/pro/webhooks/{id}, starts the count again.
happenboard_pro_webhook_auto_paused
Fires once when a webhook is switched off after too many failed deliveries in a row. Pro already uses it to email the site admin address. Hook it to warn someone else: that receiver gets nothing until the webhook is switched back on.
Signature
do_action( 'happenboard_pro_webhook_auto_paused', int $webhook_id, int $failures )
Parameters
-
$webhook_idint - ID of the webhook that was switched off.
-
$failuresint - Failed deliveries in a row when it was switched off: the limit (
happenboard_pro_webhook_auto_pause_after, 50 by default), or more for a webhook that was already past it.
Example
add_action(
'happenboard_pro_webhook_auto_paused',
function ( $webhook_id, $failures ) {
// Pro already emails the site admin: tell the developer too.
wp_mail(
'[email protected]',
'Webhook switched off',
sprintf(
'Webhook #%d failed %d times in a row and was switched off. Fix the receiver, then switch it back on in HappenBoard > Integrations > Webhooks (Pro).',
$webhook_id,
$failures
)
);
},
10,
2
);
Notes
- Two arguments are passed, so register with
10, 2. - It usually runs in the background delivery job, with no current user, right after the delivery hook of the attempt that reached the limit. For a webhook already past the limit, it fires in the admin request that lists the webhooks on the Webhooks card.
- The webhook is switched off by one conditional database update, so two deliveries failing at the same moment fire it once. Retries still waiting for that webhook stop.
- It never fires when someone switches a webhook off by hand, or when the limit filter returns
0.
happenboard_pro_slack_webhook_url_valid
Filters whether a URL may be saved as the Slack webhook. Only Slack Incoming Webhook URLs (https://hooks.slack.com/services/…) pass by default; return true to allow a Slack-compatible service such as Mattermost or Rocket.Chat.
Signature
apply_filters( 'happenboard_pro_slack_webhook_url_valid', bool $valid, string $url )
Parameters
-
$validbool - Whether the URL matches the Slack Incoming Webhook pattern.
-
$urlstring - The URL as typed, with surrounding spaces removed.
Return value
bool
True to accept the URL.
Example
add_filter(
'happenboard_pro_slack_webhook_url_valid',
function ( $valid, $url ) {
// Also accept our Mattermost incoming webhooks.
return $valid || str_starts_with( $url, 'https://chat.example.org/hooks/' );
},
10,
2
);
Notes
- Two arguments are passed, so register with
10, 2. - The URL must still start with
https://. Returning true for anhttp://URL does not get it saved. - It runs when Pro’s settings are saved, from the Slack card or
POST /happenboard/v1/pro/settings. A refused URL answers HTTP 400 withhappenboard_slack_url_invalid. Every message is still checked for a public host when it is sent. - The receiver gets Slack’s message format (
textplus Block Kitblocks) and no signature: the URL itself is the secret.
happenboard_pro_attendee_checkin_refused
Fires when the scanner turns away a genuine ticket: it is for another event, its booking holds no seat, or it is for another date of a recurring event. Nothing is checked in.
Signature
do_action( 'happenboard_pro_attendee_checkin_refused', int $attendee_id, int $event_id, string $status, int $seat_index, string $source, string $reason, int $scanned_for )
Parameters
-
$attendee_idint - Attendee whose ticket was scanned.
-
$event_idint - The event the ticket is for.
-
$statusstring - The attendee’s status at the time of the scan, such as
going,cancelled,waitlistordeclined. Onlygoingadmits. -
$seat_indexint - Seat the ticket is for, counted from 0 (0 also for one QR code covering the whole booking).
-
$sourcestring - Scan source sent by the scanner,
qrby default. -
$reasonstring wrong_event(the ticket is for another event),status(the booking holds no seat),wrong_date(another date of the series) ordate_gone(its date was skipped or removed).-
$scanned_forint - The event picked in Event at this door, or
0when the scanner accepts any event.
Example
add_action(
'happenboard_pro_attendee_checkin_refused',
function ( $attendee_id, $event_id, $status, $seat_index, $source, $reason, $scanned_for ) {
if ( 'wrong_event' !== $reason ) {
return;
}
error_log( sprintf(
'Ticket of attendee %d (event %d) was scanned at the door of event %d.',
$attendee_id,
$event_id,
$scanned_for
) );
},
10,
7
);
Notes
- Seven arguments are passed. Register with
10, 7to receive$reasonand$scanned_for. - The event is checked first, so with
wrong_eventthe status can be anything. A ticket booked on a translation of the event (WPML or Polylang) counts as the same event. - It fires only from
POST /happenboard/v1/pro/checkin/mark. Theverifyroute and thecheck-in-attendeeability can refuse a ticket too, but never fire it. - The scan answers HTTP 409 with
happenboard_pro_attendee_not_admissibleand the reason in the error data. Pro’s activity journal records it asattendee.checkin_refusedat priority 10.
happenboard_pro_attendee_checkin_undone
Fires after a mistaken check-in is undone, when door staff click Undo on the scanner. The check-in row is already deleted.
Signature
do_action( 'happenboard_pro_attendee_checkin_undone', int $checkin_id, int $attendee_id, int $event_id, int $seat_index )
Parameters
-
$checkin_idint - ID of the check-in that was undone, the one
happenboard_pro_attendee_checked_inpassed for that scan. -
$attendee_idint - Attendee the check-in was for.
-
$event_idint - Event recorded on the check-in.
-
$seat_indexint - Seat it was for, counted from 0.
Example
add_action(
'happenboard_pro_attendee_checkin_undone',
function ( $checkin_id, $attendee_id, $event_id, $seat_index ) {
// Take back the entry our own door counter added at check-in.
$count = (int) get_post_meta( $event_id, 'my_door_count', true );
update_post_meta( $event_id, 'my_door_count', max( 0, $count - 1 ) );
},
10,
4
);
Notes
- Four arguments are passed, so register with
10, 4. - It fires from
POST /happenboard/v1/pro/checkin/undo(body{ checkin_id }), open to users who can run check-in. A check-in that does not exist, or was already undone, answers HTTP 404 and fires nothing. - Pro listens three times: the activity journal adds an
attendee.checkin_undonerow (priority 10), subscribed webhooks get theattendee.checkin_undoneevent (20), and Slack posts a message when Check-in undone is ticked (30). - The webhook payload is
checkin_id,attendee_id,event_id,seat_indexandundone_utc. A receiver matchescheckin_idwith the earlierattendee.checked_indelivery.
happenboard_pro_checkin_lockout
Fires when a user is locked out of check-in after too many invalid ticket codes. A lockout means someone is probing for valid codes, or a scanner is misreading.
Signature
do_action( 'happenboard_pro_checkin_lockout', int $actor_id, int $count )
Parameters
-
$actor_idint - ID of the locked-out user,
0for a request without a signed-in user. -
$countint - Distinct failed codes that triggered the lockout: 10.
Example
add_action(
'happenboard_pro_checkin_lockout',
function ( $actor_id, $count ) {
$user = get_userdata( (int) $actor_id );
wp_mail(
get_option( 'admin_email' ),
'Check-in scanner locked',
sprintf(
'%s was locked out of check-in for 5 minutes after %d invalid codes.',
$user ? $user->display_name : 'An unknown user',
$count
)
);
},
10,
2
);
Notes
- Two arguments are passed, so register with
10, 2. - Ten distinct codes that look like HappenBoard tickets but fail verification, within 60 seconds, lock that user out for 300 seconds. Text that is not a ticket, a code already counted and a refused genuine ticket do not count.
- While locked out, verification answers HTTP 429 with
happenboard_pro_checkin_locked, and the hook does not fire again until the lockout ends. - Failures are counted per user across the scanner routes and the
verify-ticketandcheck-in-attendeeabilities. To lift a lockout early, delete thehappenboard_pro_vt_lock_{user_id}transient.
happenboard_badges_per_sheet
Filters how many name badges one badge-sheet PDF holds. A larger selection is split into several sheets, each downloaded in its own request.
Signature
apply_filters( 'happenboard_badges_per_sheet', int $per_sheet )
Parameters
-
$per_sheetint - Default
150.
Return value
int
Badges per sheet. The value is kept between 1 and 1000.
Example
add_filter(
'happenboard_badges_per_sheet',
function ( $per_sheet ) {
// Generous server: fewer, larger PDF files.
return 300;
}
);
Notes
- Only HappenBoard Pro fires it, although its name has no
_pro_part. - Badges are laid out 8 per A4 page (2 by 4), one per seat of each booking the door would admit.
- Each badge costs memory and time: 150 badges take about 4 seconds and 85 MB, 400 about 11 seconds and 280 MB. A sheet too large for the server fails with no file, so raise it only with headroom.
- Only the badge download route,
GET /happenboard/v1/pro/attendees/badges, reads it. Theexport-badgesability has its own limit of 500.
happenboard_pro_calendar_event_pushed
Fires after a HappenBoard event is written to a connected Google or Outlook calendar, on the first insert and on every later update. Use it to read the provider’s answer, such as the remote event ID or its web link.
Signature
do_action( 'happenboard_pro_calendar_event_pushed', int $post_id, string $provider, array $response )
Parameters
-
$post_idint - The HappenBoard event that was pushed.
-
$providerstring googleoroutlook. One hook serves both providers, so always check it.-
$responsearray<string,mixed> - The decoded answer. Google returns an Event resource (
id,etag,htmlLink), Microsoft Graph an event (id,@odata.etag,webLink).
Example
add_action(
'happenboard_pro_calendar_event_pushed',
function ( $post_id, $provider, $response ) {
if ( 'google' !== $provider ) {
return; // Also fires with 'outlook'.
}
update_post_meta(
(int) $post_id,
'my_gcal_html_link',
(string) ( $response['htmlLink'] ?? '' )
);
},
10,
3
);
Notes
- Three arguments are passed, so register with
10, 3. - Saving an event only queues the push, so the hook usually fires later, in a background job (Action Scheduler or WP-Cron). A manual resync fires it at once. The cancellation sent when an event is trashed, and the deletion, do not fire it.
- It fires on success only. A push that fails for a temporary reason is retried 5, 15, 60 and 240 minutes later. Temporary means no answer, a 5xx status, or 401, 403, 408, 409, 412, 423 or 429; other failures are not retried. No hook reports a failure.
- Only published events are pushed, and only once sync is on, the account connected and a target calendar chosen. An event whose own sync setting is off is skipped.
happenboard_pro_gcal_exdates
Filters the dates to leave out of a recurring event pushed to Google Calendar. They become an EXDATE line next to the RRULE.
Signature
apply_filters( 'happenboard_pro_gcal_exdates', array $exdates, int $event_id )
Parameters
-
$exdatesarray<int,string> - The dates skipped from the series in HappenBoard, each as its start in ISO 8601 UTC. Empty when no date is skipped.
-
$event_idint - The HappenBoard event being prepared for the push.
Return value
array<int,string>
ISO 8601 UTC date-times, one per date to leave out. Anything that is not an array counts as an empty list.
Example
add_filter(
'happenboard_pro_gcal_exdates',
function ( $exdates, $event_id ) {
// Skipped dates are already listed. Also keep our members-only dates
// off Google, stored as UTC starts such as 2026-11-02T18:00:00Z.
$private = get_post_meta( $event_id, 'my_private_dates', true );
if ( is_array( $private ) ) {
$exdates = array_merge( $exdates, $private );
}
return $exdates;
},
10,
2
);
Notes
- Two arguments are passed, so register with
10, 2. - The list already holds the dates skipped in HappenBoard, so they disappear from the Google copy too. Google only: the Outlook push never reads this filter.
- Return UTC values. A value that does not parse is dropped, and a value with another offset is not converted: its clock time is read as UTC.
- It runs for every event pushed, but an event with no repeat rule never gets an
EXDATEline, whatever you return: Google refuses one without anRRULE.
happenboard_pro_calendar_local_wins
Fires when the Google pull finds an event edited on both sides since the last sync and keeps the WordPress copy, because it is the more recent. Use it to log the discarded Google edit.
Signature
do_action( 'happenboard_pro_calendar_local_wins', int $post_id )
Parameters
-
$post_idint - The HappenBoard event whose WordPress copy was kept.
Example
add_action(
'happenboard_pro_calendar_local_wins',
function ( $post_id ) {
// Log only: Pro is already pushing this event back to Google.
error_log( sprintf(
'Google conflict on event %d, WordPress copy kept.',
(int) $post_id
) );
}
);
Notes
- One argument, so a plain
add_action()is enough. - When the Google push is on, Pro already listens at priority 10 and pushes the WordPress version back to Google at once. Do not push it again yourself.
- It fires only when the WordPress copy is strictly newer. On a tie, the Google version wins and the hook does not fire.
- The pull runs every 15 minutes on WP-Cron, and when someone starts a manual pull from the sync settings. The discarded Google edit is not passed to you.
happenboard_pro_calendar_local_wins_outlook
The Outlook version of happenboard_pro_calendar_local_wins. Fires when the Outlook pull finds an event edited on both sides and keeps the more recent WordPress copy.
Signature
do_action( 'happenboard_pro_calendar_local_wins_outlook', int $post_id )
Parameters
-
$post_idint - The HappenBoard event whose WordPress copy was kept.
Example
add_action(
'happenboard_pro_calendar_local_wins_outlook',
function ( $post_id ) {
// Log only: Pro is already pushing this event back to Outlook.
error_log( sprintf(
'Outlook conflict on event %d, WordPress copy kept.',
(int) $post_id
) );
}
);
Notes
- One argument, so a plain
add_action()is enough. - The hook name is the only sign of the provider: there is no
$providerargument. Listen to both hooks to cover both calendars. - When the Outlook push is on, Pro already listens at priority 10 and pushes the WordPress version back. Do not push it again yourself.
- Same rule as on Google: it fires only when the WordPress copy is strictly newer.
happenboard_pro_host_busy_unknown_blocks
Filters whether a booking host whose connected calendar could not be read is kept off the offered slots. By default, not knowing when a host is busy counts as not knowing they are free.
Signature
apply_filters( 'happenboard_pro_host_busy_unknown_blocks', bool $blocks, array $hosts )
Parameters
-
$blocksbool - Default
true. -
$hostsarray<int,int> - User IDs of the hosts whose Google or Outlook calendar could not be read for this period.
Return value
bool
True to keep those hosts off the slots, false to offer them on their HappenBoard availability alone.
Example
add_filter(
'happenboard_pro_host_busy_unknown_blocks',
function ( $blocks, $hosts ) {
// Keep offering the front desk account (user 12) even when its
// calendar cannot be read; every other host stays blocked.
return array( 12 ) === array_values( $hosts ) ? false : $blocks;
},
10,
2
);
Notes
- Two arguments are passed, so register with
10, 2. - Pro removes busy hosts from each slot’s
hostslist on the free plugin’shappenboard_booking_slotsfilter, at priority 20. A slot disappears only when no host is left. - It is called only when a calendar read failed. A failed read is cached for 60 seconds and a successful one for 300, so an outage costs a host a minute of slots at a time.
- Your answer applies to every host in
$hosts, and the same list re-checks a slot when a booking is saved. Returning false can let a booking land on a meeting you could not see. It runs on public booking pages: keep it fast.
Next steps
- Outbound webhooks: how site owners add webhooks and choose their events.
- Slack notifications: the Slack card that the URL filter applies to.
- Attendees and sign-up hooks: the check-in action and the other attendee events webhooks are built on.