Hooks & Filters
Most write operations fire a before-filter (abortable) and an after-action (full data); list endpoints expose a query-args filter and REST responses a prepare-filter. Coverage is per-resource: the matrix below lists exactly which verbs fire on each resource. Every hook name and signature on this page is verified against the shipping code. File paths are given without line numbers on purpose - a line number is stale the next time the file is touched, and a wrong one is worse than none. Grep the hook name in the cited file.
Full reference:
docs/EXTENDING.md§2 in the plugin root. This page covers the most frequently used hooks with examples.
What You Will Learn
- The
evnm_before_*/evnm_after_*naming convention - Event lifecycle hooks
- RSVP, ticket, and order hooks
- The money-path hooks that form the Free ↔ Pro contract
- Query-args and REST-prepare filters
- App config, email pipeline, capability, and feature-flag hooks
- The 1.1.0 extension seams and the event-editor seams
- Member-dashboard seams, the real scheduled (cron) hooks, and Pro-only hooks
- Settings/feature, currency, billing, template, block-render and moderation seams
- The handful of hooks whose signature does not match their name
Naming Conventions
| Pattern | Type | Use |
|---|---|---|
evnm_before_{verb}_{resource} |
Filter | Receives validated payload; return WP_Error to abort. |
evnm_after_{verb}_{resource} |
Action | Fires post-commit; receives full result. Never read $_POST. |
evnm_{resource}_query_args |
Filter | Modify read args before the query runs. |
evnm_rest_prepare_{resource} |
Filter | Add/redact/compute fields on a REST response item. |
Event Lifecycle
// Abort if the event is in the past.
add_filter( 'evnm_before_create_event', function ( array $event, array $context ) {
if ( strtotime( $event['start_utc'] ) < time() ) {
return new WP_Error( 'evnm_event_in_past', 'Events cannot start in the past.', [ 'status' => 422 ] );
}
return $event;
}, 10, 2 );
// Push to CRM after creation.
add_action( 'evnm_after_create_event', function ( array $event, array $context ) {
my_crm_push( $event['id'], $event['title'], $event['start_utc'] );
}, 10, 2 );
// Abort if capacity would drop below current RSVPs.
add_filter( 'evnm_before_update_event', function ( array $changes, array $existing, array $context ) {
if ( isset( $changes['capacity'] ) && $changes['capacity'] < $existing['rsvp_count'] ) {
return new WP_Error( 'evnm_capacity_below_rsvps', 'Capacity cannot drop below current RSVPs.', [ 'status' => 409 ] );
}
return $changes;
}, 10, 3 );
// Detect rescheduling.
add_action( 'evnm_after_update_event', function ( array $event, array $changed_keys, array $context ) {
if ( in_array( 'start_utc', $changed_keys, true ) ) {
do_action( 'my_plugin_event_rescheduled', $event['id'] );
}
}, 10, 3 );
// Archive on delete.
add_action( 'evnm_after_delete_event', function ( int $event_id, array $snapshot, array $context ) {
my_archive( $snapshot ); // full pre-delete snapshot
}, 10, 3 );
Community / group scoping (usage note). A community layer scopes a new event to a group by riding these two existing hooks; no new seam is needed. It injects a
space_idfield into the editor viaevnm_event_editor_fields(§ Editor fields), which the event-editor block carries into the create payload asPOST /events; thenevnm_before_create_eventauthorizes the write (e.g. validating that the current user is a member of that group) before the row is inserted. Pro's BuddyPress bridge uses exactly this path to stampspace_id = group_idon group-submitted events. See thespaceIdblock attribute in Blocks & Templates.
RSVP, Ticket, Order, Occurrence Lifecycle
The before_ (filter, abortable) / after_ (action, full data) pattern is real, but not every resource fires every verb. This matrix is the source of truth, and a hook not listed here does not exist:
| Resource | Create | Update | Delete | Other |
|---|---|---|---|---|
event |
✅ | ✅ | ✅ | evnm_before_duplicate_event / evnm_after_duplicate_event |
rsvp |
✅ | ✅ | ✅ | None |
ticket |
✅ | ✅ | ✅ | None |
order |
✅ | ✅ | ❌ | evnm_after_reverse_order (refund/cancel reversal); evnm_after_create_paid_attendees |
occurrence |
❌ | ✅ | ❌ | occurrences are only ever updated (soft-cancel); they materialize from the recurrence engine |
Orders are never deleted: a refunded or cancelled order reverses through evnm_after_reverse_order, not a delete hook. Occurrences fire only evnm_before_update_occurrence / evnm_after_update_occurrence (REST/Controller/OccurrencesController.php).
// Watch for paid order completion. The order row carries reserved seats in
// meta['reserved'] = [ ['ticket_id'=>int,'qty'=>int], … ] - there is no line_items column.
add_action( 'evnm_after_update_order', function ( $order, $changed_keys, $context ) {
if ( in_array( 'status', $changed_keys, true ) && 'paid' === $order['status'] ) {
foreach ( ( $order['meta']['reserved'] ?? array() ) as $seat ) {
my_grant_perk( $order['user_id'], $seat['ticket_id'] );
}
}
}, 10, 3 );
The matrix spelled out, so every name on it is greppable:
| Resource | Hooks that exist |
|---|---|
event |
evnm_before_create_event / evnm_after_create_event · evnm_before_update_event / evnm_after_update_event · evnm_before_delete_event / evnm_after_delete_event · evnm_before_duplicate_event / evnm_after_duplicate_event |
rsvp |
evnm_before_create_rsvp / evnm_after_create_rsvp · evnm_before_update_rsvp / evnm_after_update_rsvp · evnm_before_delete_rsvp / evnm_after_delete_rsvp |
ticket |
evnm_before_create_ticket / evnm_after_create_ticket · evnm_before_update_ticket / evnm_after_update_ticket · evnm_before_delete_ticket / evnm_after_delete_ticket |
order |
evnm_before_create_order / evnm_after_create_order · evnm_after_update_order · evnm_after_reverse_order · evnm_after_create_paid_attendees |
occurrence |
evnm_before_update_occurrence / evnm_after_update_occurrence · evnm_after_materialize_occurrences |
Two order-shaped absences are deliberate and worth stating: there is no
evnm_before_delete_order or evnm_after_delete_order (orders are financial records and
are never deleted - they reverse), and evnm_before_update_order is fired by Pro only
(see the order-update contract below). Note also that evnm_before_update_ticket takes its
arguments in an unusual order - see Signature Gotchas.
Duplicate lifecycle (Services/EventService.php): evnm_before_duplicate_event is a filter ($source, $overrides, $context); return WP_Error to abort. evnm_after_duplicate_event is an action ($copy, $source, $context).
Waitlist promotion: evnm_after_promote_waitlist, action ($promoted_rsvp, $context), fires when an RSVP is moved off the waitlist into a going seat (RsvpService.php).
Magic link: evnm_before_issue_magic_link, action ($rsvp_id, $request), fires just before a guest magic link is minted (RsvpController.php). There is no verify-step hook; the token in the link is the credential.
Query-Args Filters
Use these to modify SQL queries without touching the repository:
// Restrict public REST reads to published events only.
add_filter( 'evnm_events_query_args', function ( $args, $context ) {
if ( 'rest' === $context['source'] && ! is_user_logged_in() ) {
$args['status'] = 'published';
}
return $args;
}, 10, 2 );
Available: evnm_events_query_args, evnm_occurrences_query_args, evnm_rsvps_query_args, evnm_tickets_query_args, evnm_orders_query_args, evnm_catalog_query_args (venues + organizers).
REST Response Filters
Add, redact, or compute fields on every API response for that resource:
add_filter( 'evnm_rest_prepare_event', function ( $data, $event, $request ) {
$data['weather_hint'] = my_forecast( $event['start_utc'], $event['city'] );
return $data;
}, 10, 3 );
Available: evnm_rest_prepare_event, evnm_rest_prepare_occurrence, evnm_rest_prepare_rsvp, evnm_rest_prepare_ticket, evnm_rest_prepare_order, evnm_rest_prepare_venue, evnm_rest_prepare_organizer.
App Config
// Expose a feature flag to the frontend store.
add_filter( 'evnm_rest_app_config', function ( array $cfg ): array {
$cfg['features']['my_feature'] = my_feature_enabled();
return $cfg;
} );
add_filter( 'evnm_email_subject', function( $subject, $email_key, $data ) {
if ( 'rsvp_confirmation' === $email_key ) {
return 'You are going to: ' . $data['event']['title'];
}
return $subject;
}, 10, 3 );
The $email_key values that actually ship
Thirteen keys are dispatched today. The class column is UnsubscribeService::classify():
transactional mail is exempt from suppression, notification mail is suppressible and
carries the unsubscribe footer. An unlisted key defaults to notification, which is the
legally safe direction - register yours as transactional through evnm_email_classes if it
genuinely is.
$email_key |
Fired by | Class |
|---|---|---|
rsvp_confirmation |
Free - RsvpService, RsvpController |
transactional |
order_confirmation |
Free - OrderEmailProvider; Pro - OrderConfirmationProvider |
transactional |
magic_link |
Free - MagicLinkService |
transactional |
event_submitted |
Free - EventService (to the site admin) |
transactional |
event_approved |
Free - EventChangeNotificationsProvider (to the author) |
transactional |
event_rejected |
Free - EventChangeNotificationsProvider (to the author) |
transactional |
event_updated |
Free - EventChangeNotificationsProvider (to attendees) |
transactional |
event_cancelled |
Free - EventChangeNotificationsProvider (to attendees) |
transactional |
rsvp_waitlist_promoted |
Free - RsvpService |
transactional |
event_reminder |
Free - ReminderService |
notification |
event_blast |
Free - BlastProvider (organizer to attendees) |
notification (unlisted default) |
new_event |
Pro - FollowService follower fan-out |
notification |
payout_receipt |
Pro - PayoutNotificationProvider |
notification (unlisted default) |
There is no
event_rescheduledemail key.Email\Notifications::contextual_content()carries acase 'event_rescheduled'branch, but nothing dispatches that key -EventChangeNotificationsProvider::on_rescheduled()sendsevent_updated. The branch is unreachable. Do not key anevnm_email_subjectorevnm_email_bodycallback onevent_rescheduled; it will never fire. (evnm_event_rescheduled, the action, is a different thing and is real - see Event lifecycle above.)
Do not treat
bin/email-coverage-check.shas the key list. That CI gate matches string literals only, so it cannot see the four keys dispatched through a$email_keyvariable inEventChangeNotificationsProvider, and it scans Free only. Enumerate from the firing sites plusUnsubscribeService::classes(), as the table above does.
Also: evnm_email_body, evnm_email_recipients, both with the same ($value, $email_key, $data) signature (Email/EmailChannel.php). The unsubscribe layer rides evnm_email_recipients to strip suppressed addresses.
Email pipeline (rendering + delivery)
| Hook | Type | Args | Where fired | Purpose |
|---|---|---|---|---|
evnm_notify |
Action | ($recipient, $message, $context) |
every notifying provider/service | The single dispatch seam. $message['email_key'] selects the template; fan-out to registered NotificationChannels. Fire it to send mail through the same pipeline core uses (NotificationProvider.php). |
evnm_email_accent |
Filter | ($hex, $email_key) → string |
EmailChannel.php |
Brand accent colour for the HTML shell (default #007cba). |
evnm_email_html |
Filter | ($html, $subject, $body_html, $email_key, $message) |
EmailChannel.php |
Override the whole branded HTML wrapper for an email. |
evnm_email_classes |
Filter | ($classes) → map |
UnsubscribeService.php |
Map each email_key to transactional (never suppressible) / notification (suppressible). |
evnm_blast_allowed_html |
Filter | ($allowed_tags) |
Email/Notifications.php |
Allowed-HTML map for organizer event-blast bodies. |
Capabilities
// Grant co-organizers edit access.
add_filter( 'evnm_user_can_manage_event', function ( $can, $user_id, $event, $cap ) {
$co = (array) evnm_get_meta( 'event', $event['id'], 'co_organizers' );
return in_array( $user_id, array_map( 'intval', $co ), true ) ? true : $can;
}, 10, 4 );
Add-On Accessors (stable across major versions)
$currency = evnm_setting( 'currency', 'USD' );
$all = evnm_settings();
$has_wait = evnm_feature_enabled( 'waitlist', [ 'event_id' => 42 ] );
Never import \Eventonomy\Core\Settings or \Eventonomy\Core\Features directly; these accessors are the stable contract.
Formatting Filters
evnm_event_permalink-($url /* '' */, $event_id, $event_row). The canonical single-event URL filter. Resolve or override the permalink for an event id; used across blocks, calendar, and SEO (Providers/SeoProvider.php,src/blocks/events-list/render.php,src/blocks/my-events/render.php). Return a non-empty URL to supply it.evnm_event_jsonld-($data, $event, $view): modify or suppress Schema.org JSON-LD on single-event pages (SeoProvider.php).evnm_permalink_base-($base): translate or override the single-event URL slug segment (defaultevent;EventPermalinks.php). Distinct fromevnm_event_permalink, which resolves the full URL.evnm_format_money-($formatted, $amount, $args): override currency formatting (includes/functions.php).evnm_format_event_datetime-($formatted, $utc, $event_tz, $args): override date/time display (includes/functions.php).
Money-Path Hooks (the Free ↔ Pro contract)
Selling tickets is a Pro capability, but Free ships every seam Pro plugs into. These are the load-bearing hooks: the paid-order flow is assembled entirely from them, so an add-on can substitute for Pro at any point.
The paid-order lifecycle
| Hook | Type | Args | Where fired | Purpose |
|---|---|---|---|---|
evnm_payment_gateways |
Filter | ($gateways) |
OrderService, rsvp/render.php, Store, admin dashboards |
The linchpin. Free reads this to know whether ANY gateway exists; with an empty result it hides/disables the paid Order button and returns 402 on paid orders. Pro registers stripe / paypal / woo here. |
evnm_order_total |
Filter | ($subtotal, $input, $context) → float |
OrderService::create() |
Recompute the charged total. Free prices line items from the authoritative ticket map; Pro layers coupons, tax/fees, and tiered pricing on that trustworthy base. |
evnm_process_payment |
Filter | ($payment /* null */, $order, $input) → result|null |
OrderService::create() |
The charge seam. Free passes null (no gateway → the order settles only if $0); Pro's gateway returns [ 'transaction_id' => …, 'status' => … ] (the key is transaction_id, not txn_id). has_filter('evnm_process_payment') is also how the admin detects a live gateway. |
evnm_after_create_paid_attendees |
Action | ($order_id, $context) |
OrderService.php |
Fires after a paid order materializes its attendee RSVP rows. |
evnm_after_reverse_order |
Action | ($order_id, $context) |
OrderService.php |
Fires after a refunded/cancelled order releases reserved stock and voids its RSVPs. |
The order-update contract
The two halves have different owners, so listen accordingly:
evnm_before_update_orderis fired only by Pro - fromRefundService,ReconcileProviderandPaymentWebhookController. Free never fires it. A listener registered on a Free-only install will never run, so do not treat it as the matching "before" of the action below.evnm_after_update_orderis fired by both. Pro fires it from the same three places (plusWooGatewayProvider); Free fires it fromPATCH /orders/{id}andPOST /orders/bulk(REST/Controller/OrdersController.php). Free also listens to it -OrderService::reverse_on_refundand the paid-attendee materializer are both hooked here.
This is the exact seam that lets a Pro refund/webhook, or a Free admin status change, flip an order's status and have Free correctly reverse its own inventory + RSVP data through one path.
// Abort or mutate an order-status change before it commits.
add_filter( 'evnm_before_update_order', function ( array $changes, array $order, array $context ) {
// return $changes, or a WP_Error to abort.
return $changes;
}, 10, 3 );
// React after the change commits. Free's reverse_on_refund runs here.
add_action( 'evnm_after_update_order', function ( array $order, array $changed_keys, array $context ) {
if ( in_array( 'status', $changed_keys, true ) && in_array( $order['status'], array( 'refunded', 'cancelled' ), true ) ) {
// stock + RSVPs already reversed by Free; do your own thing here.
}
}, 10, 3 );
Organizer + confirmation surfaces (Pro appends UI here)
| Hook | Type | Args | Where fired | Purpose |
|---|---|---|---|---|
evnm_manage_sales_stats |
Action | ($event_id) |
src/blocks/manage-attendees/render.php |
After the core Sales stat cards. Pro appends per-event commission / net-earnings cards. |
evnm_order_admin_actions |
Action | ($order_row) |
includes/Admin/AttendeesOrdersPage.php per-row Actions cell |
Pro attaches its Refund button. The Actions column only renders when has_action() is true, so Free-alone shows no empty column. |
evnm_confirmation_ticket_qr |
Action | ($rsvp), the attendee RSVP row (carries checkin_token) |
src/blocks/my-events/render.php |
Pro's QrService renders the per-ticket scannable QR; empty on Free-alone. |
evnm_order_confirmation |
Action | ($order, $event) |
src/blocks/my-events/render.php confirmation card |
Pro fires the client-side Meta Pixel Purchase event. |
Event lifecycle (semantic transitions)
| Hook | Type | Args | Where fired |
|---|---|---|---|
evnm_event_status_changed |
Action | ($event, $old_status, $new_status, $context) |
EventService.php |
evnm_event_rescheduled |
Action | ($event, $detail, $context), where $detail = { time_changed, venue_changed, old_start_utc, new_start_utc } |
EventService.php |
Free's EventChangeNotifications provider listens on both to email registered attendees.
Member Dashboard Seams
The my-events dashboard block exposes three seams so a Pro add-on can register its own dashboard section without Free hard-coding the slug (src/blocks/my-events/render.php):
| Hook | Type | Args | Purpose |
|---|---|---|---|
evnm_dashboard_nav |
Filter | ($nav, $section, $user_id) |
Add nav items (e.g. Pro's "Saved events" / "Following"). |
evnm_dashboard_section |
Action | ($section, $user_id) |
Render the body for a slug a Pro add-on registered via evnm_dashboard_nav. |
evnm_dashboard_after_attending |
Action | ($user_id) |
Append content beneath the "Attending" list. |
New Extension Seams (1.1.0)
The 1.1.0 release adds these filters and actions. Each is verified live at the cited file:line.
| Hook | Type | Args | Where fired | Purpose |
|---|---|---|---|---|
evnm_cover_upload_max_bytes |
Filter | ($bytes) → int |
REST/Controller/MediaController.php |
Max cover-image upload size (default 5 MB). |
evnm_cover_upload_mimes |
Filter | ($mimes) → map |
REST/Controller/MediaController.php |
Allowed cover MIME map (default jpeg/png/gif/webp). |
evnm_magic_link_ttl |
Filter | ($ttl_seconds, $rsvp_id) → int |
Services/MagicLinkService.php |
Guest magic-link lifetime in seconds (default 7 days, floor 60 s). |
evnm_after_promote_waitlist |
Action | ($promoted_rsvp, $context) |
Services/RsvpService.php |
The off-the-waitlist seam (see Lifecycle above). |
evnm_invoice_rows |
Filter | ($rows, $order, $event, $lines) |
Services/PdfInvoice.php |
Typed PDF-invoice line rows before the invoice renders. |
evnm_rest_max_per_page |
Filter | ($max) → int |
REST/Controller/BaseController.php |
Per-page ceiling for every list endpoint (default 100). |
evnm_rest_max_page |
Filter | ($max_page) → int |
REST/Controller/BaseController.php |
Maximum page-number ceiling for paginated lists. |
evnm_order_receipt_page_html |
Filter | ($html, $order, $event) |
Providers/OrderAccessProvider.php |
Override the guest order-receipt page markup. |
evnm_order_receipt_base |
Filter | ($slug) → string (default order) |
Providers/OrderAccessProvider.php |
Path segment the tokenised order receipt lives under, i.e. /order/<token>/. Change it if the default collides with an existing page on your site. |
evnm_send_order_refund_email |
Filter | ($send, $order, $context) → bool |
Providers/OrderRefundEmailProvider.php |
Whether to send the buyer the refund / order-cancellation notice. Return false to substitute your own, or to stay quiet for a reversal the buyer has already seen. Pro uses it to keep an abandoned checkout silent, since that buyer never completed a purchase. |
evnm_unsubscribe_page_html |
Filter | ($html, $state, $email_hash) |
REST/Controller/UnsubscribeController.php |
Override the public unsubscribe page markup. |
evnm_events_list_before_items |
Action | ($items, $attributes) |
src/blocks/events-list/render.php |
Router-region seam that fires before the events-list items render. |
evnm_join_button_lead_seconds |
Filter | ($seconds, $event_id) → int |
src/blocks/single-event/render.php |
How long before start the virtual "Join" button goes primary (default 1 hour). |
evnm_countries |
Filter | ($map) → code => label |
Support/Countries.php |
The ISO-3166 country code → label map used by editor + address fields. |
evnm_recurrence_max_occurrences |
Filter | ($max, $rule, $window_end) → int |
Services/RecurrenceService.php |
Occurrence-expansion ceiling for a recurring series (Pro overrides the same name in AdvancedRecurrenceService.php). |
New Extension Seams (1.3.0)
The 1.3.0 release adds three public registration seams. All are additive - nothing changes without a listener.
| Hook | Type | Args | Where fired | Purpose |
|---|---|---|---|---|
evnm_admin_rail_groups |
Filter | ($groups) → array |
Admin/AdminMenu.php |
Register an extension screen in the Operations Console left rail (Rule 17: one nav shell). |
evnm_import_sources |
Filter | ($sources) → map |
Import/ImportService.php |
Register an import source (file, database, or remote). |
evnm_browse_views |
Filter | ($views) → slug => label |
Core/Settings.php::registered_views() |
Register a browse view on the Events page. |
evnm_browse_views - the browse-view registry
Settings::registered_views() is the one list of browse views. Everything that has to agree about which views exist reads it:
- the
/events/{view}/rewrite rule (Frontend/ViewPermalinks.php), - the
?evnm_view=resolverevnm_current_view(), - the view-switcher tabs in the
eventonomy/search-filterblock, - the "Enabled views" checkbox group and the Default-view select in Settings → Display.
Register a slug here and it gets all of them at once - there is no second place to edit. (Before this seam the rewrite rule hardcoded Free's four slugs, so Pro's week/day views had a switcher tab but no URL: /events/day/ 404'd and /events/week/ 301'd to whatever unrelated page's slug happened to start with "week".)
add_filter( 'evnm_browse_views', function ( $views ) {
$views = is_array( $views ) ? $views : array();
$views['week'] = __( 'Week', 'my-addon' ); // -> /events/week/
return $views;
} );
Rules:
- Register only what you can render. A registered view must have something that draws it, or the URL renders an empty page. The Events page carries
eventonomy/calendar {"view":"month"}as the switcher's calendar slot; the simplest way to own a view is to take that slot over viaevnm_block_outputwhenevnm_current_view()returns your slug (this is exactly what Eventonomy Pro does for week/day). - Free's four are reserved.
grid,list,monthandupcomingcannot be removed or relabelled through this filter; use theenabled_viewssetting to turn one off. - Rewrite rules self-heal.
ViewPermalinks::register()hashes the registered slug list and raises the existingevnm_flush_rewriteflag when it changes, so activating or deactivating your plugin re-flushes once on the next request. Never callflush_rewrite_rules()yourself on a normal request. - A newly registered view is enabled by default.
Settings::enabled_views()only treats a view as "switched off" if the owner saved Settings while that view was actually on the form (tracked inenabled_views_known), so your view works the moment your plugin activates - and stays off once the owner unticks it.
evnm_admin_rail_groups - the admin navigation seam
AdminMenu::rail_groups() returns its group array through this filter. It is the sanctioned way to put an add-on screen inside the one admin nav shell instead of leaving a stray WP submenu the user has to hunt for. Add your item to an existing group, or append your own group.
Each item is { slug, label, icon (Lucide key), url, match?, pro?, children? }; match is a list of extra ?page= slugs that should also light up the item as active.
add_filter( 'evnm_admin_rail_groups', function ( $groups ) {
$groups = is_array( $groups ) ? $groups : array();
$item = array(
'slug' => 'my-addon-screen',
'label' => __( 'My Screen', 'my-addon' ),
'icon' => 'plug',
'url' => admin_url( 'admin.php?page=my-addon-screen' ),
'match' => array( 'my-addon-screen' ),
);
// Prefer joining the group that already holds Settings.
foreach ( $groups as $i => $group ) {
foreach ( (array) ( $group['items'] ?? array() ) as $existing ) {
if ( 'eventonomy-settings' === ( $existing['slug'] ?? '' ) ) {
$groups[ $i ]['items'][] = $item;
return $groups;
}
}
}
$groups[] = array(
'label' => __( 'Configure', 'my-addon' ),
'items' => array( $item ),
);
return $groups;
} );
Reference consumer: Pro's Sync Sources page (eventonomy-pro/includes/Providers/SyncSourcesAdminProvider.php) uses exactly this pattern. Note that the filter only handles navigation - you still register the screen itself with add_submenu_page() under the eventonomy parent (priority 20, so Free's top-level menu exists first).
evnm_import_sources - the import registration seam
The registry every importer reads: the admin Importer tab, the background job runner, WP-CLI, and Pro's calendar sync all resolve sources from this one filter. Keys are source ids; values describe how to read and map the rows. See Extending for the full key reference and a worked example, and ImportRunnerInterface for executing a registered source from your own code.
New Extension Seams (1.4.1)
The 1.4.1 release adds two filters around the abandoned-checkout sweep - an hourly job that cancels pending orders which have held their reserved seats too long without completing payment. A paid-ticket flow reserves seats the moment the buyer clicks Get tickets, before the gateway redirect, so a buyer who is bounced to Stripe/PayPal and never returns leaves a pending order that permanently subtracts inventory; enough of them make an event read "full" while its tiers still show seats left. The sweep releases those seats by firing the same evnm_after_update_order transition an admin Cancel uses, so there is only ever one reversal path.
| Hook | Type | Args | Where fired | Purpose |
|---|---|---|---|---|
evnm_pending_order_ttl_minutes |
Filter | ($minutes) → int (default 60) |
Providers/StalePendingOrderProvider.php |
How long a pending order may hold its reserved seats before the sweep cancels it. Return 0 (or a negative number) to disable the sweep entirely - for a site whose "pending" means "awaiting an offline payment we mark paid by hand". Setting it to 0 also unschedules the hourly job. |
evnm_release_stale_pending_order |
Filter | ($release /* true */, $order) → bool |
Providers/StalePendingOrderProvider.php |
Decide one order at a time. Return false to exempt a specific pending order from the sweep - e.g. a bank-transfer order the owner intends to mark paid by hand - while every other stale order is still cancelled. $order is the full pending order row. |
evnm_guest_manage_link_enabled |
Filter | ($enabled /* true */) → bool |
Services/RsvpService.php, blocks/rsvp/render.php |
Whether an account-less guest gets a secure manage link (in the confirmation email, the form's "Email me a link" button, and the success-card resend). On by default and deliberately NOT tied to the magic_link feature toggle - the link is a guest's only recovery path, so a buried setting must not be able to strand them. Return false to suppress it for a site that manages every RSVP by hand. |
// Give pending orders 24 hours instead of the default 60 minutes.
add_filter( 'evnm_pending_order_ttl_minutes', fn() => 1440 );
// Keep the hourly sweep, but never touch orders flagged for manual payment.
add_filter( 'evnm_release_stale_pending_order', function ( $release, $order ) {
return empty( $order['meta']['awaiting_bank_transfer'] ) ? $release : false;
}, 10, 2 );
Event Editor Seams
The event-editor block exposes named seams so an add-on can inject fields or panels into the frontend editor without forking the template (src/blocks/event-editor/render.php):
| Hook | Type | Args | Line | Purpose |
|---|---|---|---|---|
evnm_event_editor_fields |
Filter | ($fields, $event_id) |
The real editor-fields seam: add/modify the field definitions the editor renders. | |
evnm_event_editor_after_recurrence |
Action | ($event_id) |
Append markup after the recurrence panel. | |
evnm_event_editor_after_organizer |
Action | ($event_id) |
Append markup after the organizer picker. | |
evnm_event_editor_after_venue |
Action | ($event_id) |
Append markup after the venue picker. | |
evnm_event_editor_after_location |
Action | ($event_id) |
Append markup after the location block (prefer the finer organizer/venue seams). | |
evnm_event_editor_ticket_row |
Action | ($event_id) |
Append per-ticket-row controls. | |
evnm_editor_status_options |
Filter | ($options, $fields) |
Filter the status <select> options offered in the editor. |
Scheduled (Cron) Actions
Eventonomy schedules real WP-Cron events; add listeners to these hook names, or hook the surrounding logic.
Free:
| Hook | Cadence | Registered in | Purpose |
|---|---|---|---|
evnm_daily_occurrence_maintenance |
Daily | MaterializerProvider |
Advances next_occurrence_utc pointers and re-materializes exhausted recurring series. Batch size filterable via evnm_occurrence_maintenance_batch (default 200). |
evnm_import_cleanup |
Daily | ImportProvider |
Deletes stale CSV-import artifacts. |
evnm_send_event_change_notice |
On demand (single event) | EventChangeNotificationsProvider |
Chunked WP-Cron fan-out (100 attendees/batch) that emails registered attendees when an event is cancelled/rescheduled. Args: ($event_id, $type, $offset). |
evnm_send_reminders |
Hourly | ReminderProvider |
Sweeps for occurrences due a reminder (offsets from the evnm_reminder_offsets filter, default 1 day + 1 hour) and queues the fan-out. Prefers Action Scheduler, falls back to wp_schedule_event. Unschedules itself when Pro is active; Pro's job supersedes it. |
evnm_send_reminder_batch |
On demand (single occurrence) | ReminderProvider |
Chunked reminder fan-out (100 recipients/batch). Args: ($event_id, $occurrence_id, $offset, $after). |
evnm_release_stale_pending_orders |
Hourly | StalePendingOrderProvider |
The abandoned-checkout sweep. Cancels pending orders older than evnm_pending_order_ttl_minutes (default 60) so they stop holding reserved seats, then releases the seats through the standard order-reversal path (200 orders/run, remainder picked up next hour). Prefers Action Scheduler (group eventonomy); unschedules itself when the TTL filter returns 0. |
Pro:
| Hook | Cadence | Registered in | Purpose |
|---|---|---|---|
evnm_pro_reconcile_orders |
Hourly | ReconcileProvider |
Reconciles pending gateway orders against the processor. |
evnm_pro_send_reminders |
Hourly | ReminderProvider |
Dispatches due event-reminder emails/SMS. |
evnm_pro_calendar_sync |
Hourly | CalendarSyncSchedulerProvider |
One master sweep that runs every active calendar feed whose cadence is due. Schedules itself only while at least one feed exists, and unschedules itself when the last one goes away. |
evnm_pro_sync_feed_page |
On demand (single page) | CalendarSyncSchedulerProvider |
Imports the next bounded page of one calendar feed and re-queues itself until the feed is exhausted. Args: a single array { feed_id, offset, created, skipped }. |
evnm_pro_notify_followers |
On demand (single page) | FollowServiceProvider |
Notifies the next page of an organizer's/space's/category's followers about a newly published event, then re-queues from the cursor. Args: a single array { event, context, after_id }. |
The Pro recurring jobs prefer Action Scheduler (as_schedule_recurring_action) when available and fall back to wp_schedule_event.
Job arguments are passed as ONE wrapped array
The two on-demand jobs above each receive a single array, and listeners should
register with accepted_args = 1:
add_action( 'evnm_pro_sync_feed_page', function ( $args ) {
$feed_id = (int) ( $args['feed_id'] ?? 0 );
}, 10, 1 );
This matters if you queue your own jobs alongside ours. Both Action Scheduler
and WP-Cron dispatch via do_action_ref_array( $hook, $args ), which spreads the
array's values into positional callback arguments - neither hands the callback the
array itself. So a job queued as as_enqueue_async_action( $hook, $payload ) and
wp_schedule_single_event( $t, $hook, array( $payload ) ) delivers different
arguments on each rail, and will silently work on only the one you tested. Wrap
the payload for both.
Pro-Only Hooks
These hooks are fired by Eventonomy Pro (eventonomy-pro), not Free. They are only present when Pro is active; guard listeners accordingly. File:line anchors are in the eventonomy-pro repo.
Follows
| Hook | Type | Args | Where fired |
|---|---|---|---|
evnm_before_follow |
Filter | ($gate /* true */, $user_id, $type, $object_id); return falsy/WP_Error to block |
Services/FollowService.php |
evnm_before_unfollow |
Filter | ($gate /* true */, $user_id, $type, $object_id) |
Services/FollowService.php |
evnm_after_follow |
Action | ($user_id, $type, $object_id) |
Repository/FollowRepository.php |
evnm_after_unfollow |
Action | ($user_id, $type, $object_id) |
Repository/FollowRepository.php |
evnm_after_follow_notify |
Action | ($user_id, $type, $object_id, $notify) |
Repository/FollowRepository.php |
Earnings & payouts
| Hook | Type | Args | Where fired |
|---|---|---|---|
evnm_pro_commission_rate |
Filter | ($rate, $organizer_id, $event_id, $order) → float (percent) |
Providers/EarningsProvider.php |
evnm_pro_platform_fee_flat |
Filter | ($flat, $organizer_id, $event_id, $order) → float (flat fee, MAJOR units; the fixed-mode counterpart of evnm_pro_commission_rate) |
Providers/EarningsProvider.php |
evnm_after_book_earning |
Action | ($earning, $order) |
Providers/EarningsProvider.php |
evnm_after_reverse_earning |
Action | ($order_id, $order) |
Providers/EarningsProvider.php |
evnm_earnings_stats_html |
Filter | ($html, $event_id, $sum) |
Providers/EarningsProvider.php |
evnm_after_payout |
Action | ($organizer_id, $result) - $result = { payout_id, organizer_id, paid_count, gross, net, debt_applied, debt_remaining, currency, reference }. Fired ONCE per payout from the PayoutService chokepoint (CLI, admin UI, REST all route through it). currency is the real payout currency; payout_id + reference are additive. The money-out seam: notify, write to an external ledger, or trigger a real transfer. |
Services/PayoutService.php |
evnm_after_record_payout_debt |
Action | ($order_id, $organizer_id, $amount, $currency) |
Repository/EarningsRepository.php |
evnm_after_settle_payout_debt |
Action | ($organizer_id, $result) |
Repository/EarningsRepository.php |
Coupons
| Hook | Type | Args | Where fired |
|---|---|---|---|
evnm_after_coupon_redeem |
Action | ($code, $order_id, $used) |
Services/CouponService.php |
evnm_after_coupon_release |
Action | ($key, $order_id) |
Services/CouponService.php |
evnm_pro_coupon_hold_minutes |
Filter | ($minutes) → int (default 60, floor 5) |
Services/CouponService.php |
Check-in
| Hook | Type | Args | Where fired |
|---|---|---|---|
evnm_after_checkin |
Action | ($rsvp_row, $event_id) |
Services/CheckinService.php |
evnm_rest_prepare_checkin |
Filter | ($row) |
Services/CheckinService.php |
evnm_checkin_page_html |
Filter | ($html, $title, $body, $ok) |
Providers/CheckinUrlProvider.php |
Calendar sync
Fired by Pro's calendar-sync feature. Internals: Calendar Sync Internals.
| Hook | Type | Args | Where fired |
|---|---|---|---|
evnm_pro_calendar_sync_batch |
Filter | ($max) → int (default 50) |
Providers/CalendarSyncSchedulerProvider.php - feeds processed per hourly sweep. |
evnm_pro_sync_default_author |
Filter | ($user_id) → int (default 1) |
Services/FeedRunner.php - author for site-wide feeds (owner_id 0). Member-owned feeds author as the member. |
evnm_pro_member_feed_cap |
Filter | ($cap, $owner_id) → int (default 20) |
REST/CalendarFeedController.php - how many feeds one member may save. |
Reminders, SMS, geocoding, reports, maps, dashboard
| Hook | Type | Args | Where fired |
|---|---|---|---|
evnm_pro_reconcile_cutoff |
Filter | ($seconds) → int (default 15 min, floor 60 s) |
Providers/ReconcileProvider.php |
evnm_pro_reminder_hours |
Filter | ($hours) → int (default 24, floor 1) |
Providers/ReminderProvider.php |
evnm_pro_sms_validate |
Filter | ($ok /* true */, $to, $recipient) |
Services/SmsChannel.php |
evnm_geocode_cache_ttl |
Filter | ($ttl, $address, $provider) |
Services/GeocodingService.php |
evnm_geocode_request_args |
Filter | ($args, $address, 'nominatim') |
Services/GeocodingService.php |
evnm_admin_reports_cards |
Filter | ($cards) |
Providers/AdminReportsProvider.php |
evnm_archive_map_html |
Filter | ($html, $located, $attributes) |
Providers/MapDefaultsProvider.php |
evnm_dashboard_section_html |
Filter | ($html, $section) |
Providers/DashboardSectionsProvider.php |
Scale and Notification Seams (1.3.0)
Four seams that exist because of specific bug classes. Each is small, and each is the sanctioned way to do something that is otherwise easy to get wrong.
evnm_notify_bulk (action)
do_action( 'evnm_notify_bulk', array $recipients, array $message, array $context )
Dispatches ONE notification for a whole batch of recipients instead of firing
evnm_notify once each. Channels that can genuinely batch - push, the in-app feed -
collapse the set into a single HTTP call or a multi-row insert; email still goes out
per mailbox, because there is nothing to batch about one address.
A channel opts in by implementing BulkNotificationChannelInterface::send_bulk( array $recipients, array $message, array $context ). Channels that do not implement
it are looped automatically, so adopting it is optional and nothing breaks. Free's
InAppChannel and Pro's ExpoPushChannel both implement it.
Every multi-recipient fan-out now uses it. The four call sites are Free's event
blast (BlastProvider), event-change notices (EventChangeNotificationsProvider) and
reminders (ReminderService), plus Pro's follower fan-out (FollowService). Each fires
evnm_notify_bulk once per batch, not once per recipient.
Per-recipient values still work: each entry in $recipients carries its own
personalize map, so {guest_name} and friends resolve per person inside a single
dispatch. The remaining evnm_notify call sites are all genuinely single-recipient
(one RSVP confirmation, one magic link, one waitlist promotion, one payout receipt),
which is why they were left alone.
evnm_import_source_failed (action)
do_action( 'evnm_import_source_failed', string $reason, string $context, string $source_id )
ImportSourceInterface::rows() returns a plain array, so a dead feed and an empty
feed look identical to the runner. That is what let a broken calendar report a
healthy sync for weeks. A source raises this when it fails - unreachable host,
non-200, unparseable body, size cap - and the caller turns the first reason into a
real failure with a human-readable message.
If you write an import source, fire it on every failure path. Returning an empty array silently is the bug, not the fallback.
evnm_registration_modes (filter)
apply_filters( 'evnm_registration_modes', array $modes ) - mode => label.
The three built-in modes are rsvp, external and none. Add one here if you need
a fourth. Note that ticketing is deliberately not a mode: an event is ticketed
because it has ticket rows, which is already the source of truth, and a tickets
mode would be a second one that can contradict the first.
Read an event's mode with evnm_event_registration_mode( array $event ): string
(unknown values fall back to rsvp rather than silently disabling registration on a
live event), and ask the single gate evnm_event_accepts_registration( array $event )
before creating attendance of any kind.
evnm_iterate_rows() (function)
evnm_iterate_rows( callable $fetch, int $per_page = 100, int $max_pages = 1000 ): Generator
Every repository query() clamps per_page to 100, and a clamped page is
indistinguishable from a complete one. Asking for 500 and looping the result gives
you the first 100 rows and no error - which is how a refund once voided only the
first 100 attendee tickets while the rest stayed valid and scannable.
Use this walker whenever you need all matching rows:
foreach ( evnm_iterate_rows(
static fn( $after, $per_page ) => $repo->query( array(
'event_id' => $event_id,
'per_page' => $per_page,
'after' => $after, // cursor mode
) )
) as $row ) {
// every row, not just the first page
}
It hands the fetcher the previous page's last row, so it works with an integer
keyset or a composite one such as (start_utc, id). It is a global function
precisely so Pro can use it without importing a Free concrete class.
evnm_pro_round_money() (function, Pro)
evnm_pro_round_money( float $amount, string $currency ): float
Rounds to the currency's real ISO minor unit - 0 places for JPY, 3 for KWD - not
an assumed 2. Use it for any money value; use plain round( $x, 2 ) only for
percentages, which really are 2dp. Blanket 2dp rounding on money produced fractional
yen (unpayable) and truncated dinar.
Settings, Features and Defaults
Every setting and feature flag passes through a filter before it is read, so an add-on can change a default without touching the database.
| Hook | Type | Args | Where fired | Purpose |
|---|---|---|---|---|
evnm_default_settings |
Filter | ($defaults) → key => value |
Core/Settings.php |
The full default-settings map. Add your own key here and evnm_setting() will resolve it. |
evnm_default_features |
Filter | ($defaults) → slug => bool |
Core/Features.php |
The default on/off state of every feature flag. |
evnm_feature_{$key}_enabled |
Filter | ($enabled, $context) → bool |
Core/Features.php |
Dynamic. Decide one feature per call, with context. $key is the feature slug (rsvp, calendar, magic_link, recurrence, waitlist, ics, custom_questions, geocoding, analytics, sms, discovery, reminders). $context is whatever the caller passed to evnm_feature_enabled(), typically [ 'event_id' => 42 ] - which is how you turn a feature on for one event only. |
Settings-screen seams
The Settings screen exposes four render seams so an add-on can add its own controls to an existing tab instead of registering a separate page.
| Hook | Type | Args | Purpose |
|---|---|---|---|
evnm_settings_after_tab |
Action | ($tab) |
Fires after every tab renders; branch on $tab yourself. |
evnm_settings_after_{$tab} |
Action | none | Dynamic. The same moment, scoped to one tab slug, so you do not need the branch. |
evnm_settings_after_registration_payments |
Action | none | The Registration and Payments card specifically - the one an add-on gateway usually wants. |
evnm_settings_feature_{$key} |
Action | ($enabled) |
Dynamic. Inside a feature card's body, so a Pro feature can render its own config under its own toggle. $key is the feature slug. |
evnm_settings_email_templates |
Action | none | The Email tab's template area; Pro's template builder mounts here. |
Currency
Money formatting resolves through three filters. Change the registry rather than
re-implementing formatting, or evnm_format_money() and the client-side checkout preview
will disagree.
| Hook | Type | Args | Where fired | Purpose |
|---|---|---|---|---|
evnm_currency_registry |
Filter | ($registry) |
Support/Currencies.php |
The whole ISO-4217 registry. Add a currency here to make it selectable everywhere. |
evnm_currency_symbols |
Filter | ($symbols) → code => symbol |
Support/Currencies.php |
Override just the display symbol. |
evnm_currency_decimals |
Filter | ($decimals, $code) → int |
Support/Currencies.php |
The currency's real minor units, used for money maths. This is not the display-precision setting; changing it changes what amounts mean. |
Billing and Buyer Identity
Billing identity lives on the WordPress user (on WooCommerce's billing_* meta keys),
not on the order, so one saved address serves every purchase.
| Hook | Type | Args | Where fired | Purpose |
|---|---|---|---|---|
evnm_billing_fields |
Filter | ($fields) |
includes/functions.php |
The billing form field definitions used by /me/billing and checkout. |
evnm_billing_address |
Filter | ($address, $user_id) |
includes/functions.php |
The resolved address for one user, before it is used or returned. |
evnm_billing_address_saved |
Action | ($user_id, $address) |
includes/functions.php |
After a save. Sync to a CRM or tax service here. |
evnm_buyer_account_created |
Action | ($user_id, $email) |
includes/functions.php |
A buyer account was minted during checkout. Use it to send a set-password mail or apply a role. |
Templates and View Data
| Hook | Type | Args | Where fired | Purpose |
|---|---|---|---|---|
evnm_view_data |
Filter | ($data, $type, $id) |
includes/template-loader.php |
The bundle evnm_get_view_data() returns ([ 'event' => row, 'occurrences' => [...] ]). Add data your own template override needs instead of querying inside the template. |
evnm_template_part |
Filter | ($name, $args) → string |
includes/template-loader.php |
Swap which template part is loaded. |
evnm_template_part_args |
Filter | ($args, $name) |
includes/template-loader.php |
Change the variables a part receives. |
Frontend Render Seams
Block-level seams beyond the editor and dashboard ones above. All fire from
src/blocks/<block>/render.php.
| Hook | Type | Args | Block | Purpose |
|---|---|---|---|---|
evnm_events_grid_columns |
Filter | ($columns) → int (default 3) |
events-list |
Grid column count. |
evnm_calendar_max_pages |
Filter | ($pages) → int (default 40) |
calendar |
Ceiling on the keyset walk that builds a month. Raise it only if a single month genuinely holds more occurrences than 40 pages. |
evnm_upcoming_view_all_url |
Filter | ($url) |
upcoming |
Where the "view all" link points (default /events/). |
evnm_single_event_actions |
Action | ($event_id, $event) |
single-event |
Append action buttons beside the core ones. |
evnm_single_event_map_embed |
Filter | ($html, $context) where $context = { lat, lng, event, id } |
single-event |
Replace the map embed markup (Pro swaps in its own provider). |
evnm_render_event_organizers |
Action | ($event, $event_id) |
single-event |
Render your own organizer block in place of / alongside the core one. |
evnm_render_event_venues |
Action | ($event, $event_id) |
single-event |
The venue counterpart. |
evnm_rsvp_collect_phone |
Filter | ($collect, $event_id) → bool (default false) |
rsvp |
Ask for a phone number on the RSVP form. Turn this on if you deliver SMS. |
evnm_event_create_link |
Filter | ($url /* '' */, $user_id) |
search-filter |
Supply the "create event" URL; empty string means no button. |
evnm_event_edit_link |
Filter | ($url /* '' */, $event_id, $event) |
my-events |
Supply the edit URL for a dashboard row. |
evnm_event_manage_link |
Filter | ($url /* '' */, $event_id, $event) |
my-events |
Supply the manage-attendees URL for a dashboard row. |
Notification Internals
evnm_notify / evnm_notify_bulk are the seams you fire. These two are the seams you
observe or reshape once a dispatch is under way.
| Hook | Type | Args | Where fired | Purpose |
|---|---|---|---|---|
evnm_notification_dispatch |
Action | ($recipient, $message, $context) |
Services/NotificationService.php |
Fires once per resolved recipient, after personalization and after suppression has been applied. The audit/logging seam - do not send from here or the message goes twice. |
evnm_reminder_notification |
Filter | ($message, $payload, $occurrence, $offset) |
Services/ReminderService.php |
Reshape one reminder before it is dispatched. It is applied per recipient even inside a bulk dispatch: whatever you change relative to the shared base is diffed onto that recipient's personalize, which is how Pro adds a per-person sms body and phone. $offset is the reminder offset in seconds before start. |
Moderation, Abuse and Access
| Hook | Type | Args | Where fired | Purpose |
|---|---|---|---|---|
evnm_event_reported |
Action | ($event_id, $report) where $report = { reason, note, user_id, reported_at } |
REST/Controller/ReportController.php |
A member flagged an event. Build an external moderation queue here. |
evnm_report_resolution_changed |
Action | ($event_id, $report_key, $record) |
REST/Controller/ModerationController.php |
A report was resolved or re-opened. $record is the stored report after the change. |
evnm_user_is_banned |
Filter | ($banned, $user_id) → bool |
Providers/BanGateProvider.php |
The ban gate. Return true to block a user from creating or RSVPing. Delegate to your own moderation plugin here. |
evnm_user_ban_changed |
Action | ($user_id, $banned) |
Providers/BanGateProvider.php |
A ban was applied or lifted. |
evnm_rate_limit_{$key} |
Filter | ([ 'limit' => int, 'window' => int ]) |
Support/RateLimiter.php |
Dynamic. Retune one throttle bucket. $key names the bucket (rsvps, orders, magic_link, …). Return both keys; window is in seconds. |
Service and Repository Internals
| Hook | Type | Args | Where fired | Purpose |
|---|---|---|---|---|
evnm_event_collect_input |
Filter | ($input, $request) |
REST/Controller/EventsController.php |
The normalized event input assembled from a REST request, before it reaches EventService. Use this to accept a field of your own on POST/PATCH /events; use evnm_before_create_event to validate the row that results. |
evnm_recurrence_rule |
Filter | ($rule, $event) |
Services/OccurrenceMaterializer.php |
The recurrence rule immediately before expansion. Cheaper than writing an engine when you only need to adjust a rule. |
evnm_after_apply_event_terms |
Action | ($event_id, $input, $context) |
Services/EventService.php |
Categories and tags have been written. Mirror them into your own taxonomy here. |
evnm_after_materialize_occurrences |
Action | ($event_id) |
Services/OccurrenceMaterializer.php |
A series finished (re-)materializing. Anything cached per-occurrence should be invalidated here. |
evnm_fallback_event_author |
Filter | ($user_id, $context) → int |
Services/EventService.php |
Who owns an event created with no resolvable author (import, CLI, cron). |
evnm_free_owns_reminders |
Filter | ($free_owns) → bool |
Providers/ReminderProvider.php |
Whether Free schedules the reminder sweep. Defaults to "yes unless Pro is active". Return false if your add-on takes reminders over, or Free and your add-on will both send. |
evnm_user_display_names |
Filter | ($names, $user_ids) → id => name |
Admin/ReportedEventsPage.php |
Batch-resolve display names. Fill it to avoid a per-row get_userdata() when your community layer already has them. |
evnm_user_can_create_events |
Filter | ($can, $user_id) → bool |
includes/capability-api.php |
The final word on the create gate, after the capability and the creator_role setting. |
Bootstrap and Registry
| Hook | Type | Args | Where fired | Purpose |
|---|---|---|---|---|
evnm_register_services |
Action | ($container) |
Core/Plugin.php |
Fires after every provider file is loaded. Bind or tag your own services here - see Recipe: Extend with a Provider. |
evnm_blocks |
Filter | ($blocks) → name => [ 'path' => dir ] |
Blocks/BlockRegistrar.php |
Register a block. See Blocks & Templates. |
evnm_integrations |
Filter | ($integrations) |
Integrations/IntegrationsRegistry.php |
Advertise an integration on the Integrations screen. |
evnm_admin_reports |
Action | none | Admin/ReportsPage.php |
Append a section to the admin Reports screen. (Pro's card-level seam is evnm_admin_reports_cards.) |
evnm_use_fullwidth_template |
Filter | ($use) → bool |
Frontend/TemplateInclude.php |
Opt out of the bundled full-width page template. Classic themes only - on a block theme the swap never happens, so this filter is unreachable there. |
evnm_rest_me |
Filter | ($data, WP_User $user) |
REST/Controller/SettingsController.php |
Extend the GET /me payload. |
evnm_pro_upgrade_url |
Filter | ($url) |
Admin/UpgradePage.php |
Where the upgrade CTA points. |
evnm_free_license_key |
Filter | ($key) |
eventonomy.php |
The Free licence key used for update checks. |
Pro-Only Hooks: Calendar Sync, Connect and Import
These complete the Pro tables above.
| Hook | Type | Args | Where fired | Purpose |
|---|---|---|---|---|
evnm_pro_calendar_sync_time_budget |
Filter | ($seconds) → int (default 45) |
Providers/CalendarSyncSchedulerProvider.php |
Wall-clock budget for one sync sweep. The sweep stops and re-queues when it is spent, which is what keeps a slow remote feed from blowing the request. |
evnm_pro_member_sync_enabled |
Filter | ($enabled) → bool (default true) |
Providers/CalendarSyncDashboardProvider.php |
Whether members see the calendar-sync UI on their dashboard at all. Return false to make sync admin-only. |
evnm_pro_connect_redirect_uri |
Filter | ($url) |
Services/ConnectService.php, Providers/PaymentSettingsProvider.php |
The Stripe Connect OAuth return URI (default rest_url( 'eventonomy/v1/pro/connect/return' )). If you change it, register the same value in the Stripe dashboard - the settings screen shows whatever this filter returns, so the two stay in step. |
evnm_pro_import_walk_ttl |
Filter | ($seconds) → int (default 1 hour) |
Import/WalkCache.php |
How long a paged import's walk cache survives between batches. |
Signature Gotchas
A handful of hooks do not follow the pattern their name suggests. These are real and verified; type-hint accordingly or your callback will fatal.
evnm_event_permalinkhas two shapes. Almost every call site passes('' , $event_id, $event_row), but two pass only two arguments with a non-empty default: thesingle-eventblock (get_permalink(), $event_id) and Pro's Stripe gateway (home_url('/'), $event_id). A callback that declares three required parameters will error at those two sites. Declare the third as optional.evnm_rest_prepare_organizerandevnm_rest_prepare_venuetake two arguments ($data, $row) while every otherevnm_rest_prepare_*takes three. Do not register them withaccepted_args = 3expecting a$request.evnm_rest_prepare_eventcan receivenullfor$request. Pro's BuddyPress member-events query runs rows through the filter outside a REST request. Never type-hint that parameter asWP_REST_Request; checkinstanceofif you need it.evnm_before_update_ticketargument order is($changes, $context, $id)- context second, id third. Every otherevnm_before_update_*puts the existing row second and the context last.evnm_recurrence_max_occurrencesis fired by both Free and Pro with different defaults. One callback runs against both expansion paths, so return a value that is correct for either rather than assuming which engine you are in.
What's Next?
Learn how to work with blocks, templates, and the Interactivity store.