Eventonomy

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_id field into the editor via evnm_event_editor_fields (§ Editor fields), which the event-editor block carries into the create payload as POST /events; then evnm_before_create_event authorizes 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 stamp space_id = group_id on group-submitted events. See the spaceId block 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;
} );

Email

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_rescheduled email key. Email\Notifications::contextual_content() carries a case 'event_rescheduled' branch, but nothing dispatches that key - EventChangeNotificationsProvider::on_rescheduled() sends event_updated. The branch is unreachable. Do not key an evnm_email_subject or evnm_email_body callback on event_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.sh as the key list. That CI gate matches string literals only, so it cannot see the four keys dispatched through a $email_key variable in EventChangeNotificationsProvider, and it scans Free only. Enumerate from the firing sites plus UnsubscribeService::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 (default event; EventPermalinks.php). Distinct from evnm_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_order is fired only by Pro - from RefundService, ReconcileProvider and PaymentWebhookController. 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_order is fired by both. Pro fires it from the same three places (plus WooGatewayProvider); Free fires it from PATCH /orders/{id} and POST /orders/bulk (REST/Controller/OrdersController.php). Free also listens to it - OrderService::reverse_on_refund and 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= resolver evnm_current_view(),
  • the view-switcher tabs in the eventonomy/search-filter block,
  • 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 via evnm_block_output when evnm_current_view() returns your slug (this is exactly what Eventonomy Pro does for week/day).
  • Free's four are reserved. grid, list, month and upcoming cannot be removed or relabelled through this filter; use the enabled_views setting to turn one off.
  • Rewrite rules self-heal. ViewPermalinks::register() hashes the registered slug list and raises the existing evnm_flush_rewrite flag when it changes, so activating or deactivating your plugin re-flushes once on the next request. Never call flush_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 in enabled_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_permalink has two shapes. Almost every call site passes ('' , $event_id, $event_row), but two pass only two arguments with a non-empty default: the single-event block (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_organizer and evnm_rest_prepare_venue take two arguments ($data, $row) while every other evnm_rest_prepare_* takes three. Do not register them with accepted_args = 3 expecting a $request.
  • evnm_rest_prepare_event can receive null for $request. Pro's BuddyPress member-events query runs rows through the filter outside a REST request. Never type-hint that parameter as WP_REST_Request; check instanceof if you need it.
  • evnm_before_update_ticket argument order is ($changes, $context, $id) - context second, id third. Every other evnm_before_update_* puts the existing row second and the context last.
  • evnm_recurrence_max_occurrences is 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.

Blocks & Templates →