Event Espresso Checkout Dynamic Hooks: CRM-Ready Guide to Data Extraction


How to capture attendee, registration, transaction, and payment data at each stage of the Event Espresso Single Page Checkout (SPCO) flow.




Table of Contents

  1. Quick Cheat Sheet
  2. When Data Becomes Available
  3. How Dynamic Hook Names Are Composed
  4. Hook Reference Map
  5. Concrete Examples
  6. Decision Guide: Which Hook for Which CRM Use-Case
  7. Production Notes
  8. Common Pitfalls
  9. Code Location References
  10. Hook Firing Flowchart



0. Quick Cheat Sheet

Goal Recommended Hook Notes
Lead capture (pre-payment) AHEE__Single_Page_Checkout__after_attendee_information__process_reg_step   Fires even if validation fails; check $reg_step->completed()   before using data. For guaranteed-saved data, use AHEE__EE_Single_Page_Checkout__process_attendee_information__end   instead
Paid conversion (confirmed) AHEE__EE_Payment_Processor__update_txn_based_on_payment__successful   Payment approved; can be async (IPN); paid-in-full depends on remaining balance
Order finalized (SPCO path) AHEE__EE_SPCO_Reg_Step_Finalize_Registration__process_reg_step__completed   SPCO flow finished; may not wait for IPN
All payment updates (any gateway) AHEE__EE_Payment_Processor__update_txn_based_on_payment   Fires on every payment update
Any registration status change AHEE__EE_Registration__set_status__after_update   Catches admin changes and status transitions (cancellations/declines/approvals)
Browser-only tracking AHEE__thank_you_page_overview_template__top   User is in browser; safe for pixels

Off-site gateways often confirm payment after the user returns (IPN/webhook), so "finalize step completed" does not equal "payment confirmed."


Hook Selection Decision Tree

  • Need to act only when money is confirmed (note: not necessarily paid-in-full)? Use AHEE__EE_Payment_Processor__update_txn_based_on_payment__successful  .
  • Need to capture attendees even if they abandon checkout? Use AHEE__Single_Page_Checkout__after_attendee_information__process_reg_step   — but always check $reg_step->completed()   since this hook fires even on failed validation.
  • Need to react to admin changes or status transitions? Use AHEE__EE_Registration__set_status__after_update  .
  • Need to fire client-side pixels? Use AHEE__thank_you_page_overview_template__top  .



1. When Data Becomes Available

The SPCO flow has two to four registration steps, depending on active add-ons and event configuration, each step making progressively more data available:


Step 1: attendee_information   (Registration Form)

The attendee fills in personal details and answers event-specific questions.


Data available after this step completes:


Object Access Notes
EE_Transaction   $checkout->transaction   Created when the user proceeds to registration checkout and SPCO first loads
EE_Registration[]   $checkout->transaction->registrations()   One per ticket; created when SPCO first loads
EE_Ticket   $registration->ticket()   Relation added when registration created
EE_Event   $registration->event()   Relation added when registration created
EE_Attendee   $registration->attendee()   Created or updated during this step's process_reg_step()  
EE_Answer[]   $registration->answers()   Question responses saved during processing

Not yet available: Payment, payment method, billing info. Transaction status is typically EEM_Transaction::incomplete_status_code  . Registration status may still be pending/incomplete until finalize/payment hooks fire.


Step 2: payment_options   (Payment)

The attendee selects a payment method and submits payment (on-site), or is redirected to an off-site gateway. This step is skipped for free events.


Additional data available after this step:


Object Access Notes
EE_Payment   $checkout->payment   Created after payment attempt
EE_Payment_Method   $checkout->payment_method   The selected gateway
EE_Billing_Info_Form   $checkout->billing_form   Billing form data (on-site only)

Caveat: For off-site gateways, the payment object may not be finalized until an IPN (Instant Payment Notification) arrives asynchronously. Billing form data only exists for on-site gateways, so treat it as optional. For free events, this step will not have an associated EE_Payment   object.


Step 3: finalize_registration   (Finalization)

This step has no user-facing form. It runs automatically after payment to:

  • Update the transaction status based on payment
  • Toggle registration statuses (e.g., pending to approved)
  • Trigger notification emails
  • Redirect to the Thank-You page

All data is typically finalized:


Object Access Notes
EE_Transaction   (updated) $checkout->transaction   Status reflects payment outcome
EE_Registration[]   (updated) $checkout->transaction->registrations()   Statuses updated (approved, pending, etc.)

Note: For off-site gateways, payment confirmation may still arrive later via IPN/webhook.


Thank-You Page (Post-Checkout)

After SPCO, the user lands on the Thank-You page. The transaction is loaded fresh from the database. For off-site gateways, an AJAX polling mechanism checks for IPN completion.




2. How Dynamic Hook Names Are Composed

Many SPCO hooks are dynamically generated at runtime using the current step slug and action. Understanding how these names are built is essential for hooking into the correct moment.


Source of Truth

Reg Step slug is set from the step   request parameter:


File: modules/single_page_checkout/EED_Single_Page_Checkout.module.php

$this->checkout->step = $this->request->getRequestParam('step', $this->_get_first_step());

Default: 'attendee_information'   (the first step).


Action is set from the action   request parameter:


File: modules/single_page_checkout/EED_Single_Page_Checkout.module.php

$this->checkout->action = $this->request->getRequestParam('action', 'display_spco_reg_step');

Default: 'display_spco_reg_step'  .


For AJAX requests, the action is overridden by the static handler that dispatches the request.


AJAX Handler Sets Action To
EED_Single_Page_Checkout::display_reg_step()   'display_spco_reg_step'  
EED_Single_Page_Checkout::process_reg_step()   'process_reg_step'  
EED_Single_Page_Checkout::update_reg_step()   'update_reg_step'  

Confirm your site's dispatch path before relying on it.


Valid Step Slugs (Default Configuration)

Slug Class Order
attendee_information   EE_SPCO_Reg_Step_Attendee_Information   10
payment_options   EE_SPCO_Reg_Step_Payment_Options   30
finalize_registration   EE_SPCO_Reg_Step_Finalize_Registration   999

Add-ons can register additional steps via the AHEE__SPCO__load_reg_steps__reg_steps_to_load   filter. If aregistration_confirmation   step is present in the loaded array, it is removed in _load_and_instantiate_reg_steps()  .

One filter controls the step definitions array before instantiation; the other filters the instantiated step objects:

Phase Filter What it changes
Before instantiation AHEE__SPCO__load_reg_steps__reg_steps_to_load   Raw step definitions array
After instantiation FHEE__Single_Page_Checkout__load_reg_steps__reg_steps   Instantiated reg step objects

Valid Actions

Action When Used
display_spco_reg_step   Displaying a step's form (default)
process_reg_step   Submitting/processing a step's form
update_reg_step   Revisiting to update a previously completed step
redirect_form   Off-site gateway redirect

Dynamic Hook Pattern

The _process_form_action()   method fires three dynamic hooks around each step action. Note that the filter hook

only fires in the default branch (not for display_spco_reg_step  ), and the after   hook fires even if the action

method is missing or the filter returns false  .

If you hook only into the filter, it will not run on display-only requests.


Important: the filter hook always includes the literal process_reg_step   segment, even when the current action

is update_reg_step  .

The filter can return false   to skip calling the step method.

In the code path, the filter is called inside the non-display branch (default:  ), just before calling the step method.

See _process_form_action()   in modules/single_page_checkout/EED_Single_Page_Checkout.module.php  .


AHEE__Single_Page_Checkout__before_{SLUG}__{CURRENT_ACTION}
AHEE__Single_Page_Checkout__process_reg_step__{SLUG}__{CURRENT_ACTION}  (filter, despite AHEE prefix)
AHEE__Single_Page_Checkout__after_{SLUG}__{CURRENT_ACTION}

This is the exact call from _process_form_action()  :


$process_reg_step = apply_filters(
    "AHEE__Single_Page_Checkout__process_reg_step__{$this->checkout->current_step->slug()}__{$this->checkout->action}",
    true,
    $this->checkout->current_step,
    $this
);

Example matrix (why the double process_reg_step  is correct):


Current action Filter hook includes
process_reg_step   ...__process_reg_step__{slug}__process_reg_step  
update_reg_step   ...__process_reg_step__{slug}__update_reg_step  

Example for processing attendee information:


Slug:   attendee_information
Action: process_reg_step (current action)

Hooks that fire:
  AHEE__Single_Page_Checkout__before_attendee_information__process_reg_step     (action)
  AHEE__Single_Page_Checkout__process_reg_step__attendee_information__process_reg_step  (filter)
  AHEE__Single_Page_Checkout__after_attendee_information__process_reg_step      (action)

Example for updating attendee information:


Slug:   attendee_information
Action: update_reg_step (current action)

Hooks that fire:
  AHEE__Single_Page_Checkout__before_attendee_information__update_reg_step     (action)
  AHEE__Single_Page_Checkout__process_reg_step__attendee_information__update_reg_step  (filter)
  AHEE__Single_Page_Checkout__after_attendee_information__update_reg_step      (action)

This is the pattern used in the ee-code-snippet-library example:


add_action(
    'AHEE__Single_Page_Checkout__after_attendee_information__process_reg_step',
    'ee_spco_change_line_item_before_payment_options_step',
    10,
    1
);
function ee_spco_change_line_item_before_payment_options_step(EE_SPCO_Reg_Step_Attendee_Information $reg_step)
{
    // $reg_step->checkout->transaction is available here
    // Attendee data has just been saved
}

Initialization Hook Pattern

Each step also fires a per-step initialization hook during _initialize_reg_steps()  :


AHEE__Single_Page_Checkout___initialize_reg_step__{SLUG}

Example:

AHEE__Single_Page_Checkout___initialize_reg_step__attendee_information
AHEE__Single_Page_Checkout___initialize_reg_step__payment_options
AHEE__Single_Page_Checkout___initialize_reg_step__finalize_registration



3. Hook Reference Map

3.1 SPCO Lifecycle Hooks (typical firing order)

Order varies by request path (display vs process vs async IPN) and early exits.


Initialization Phase

Hook Type Params When
FHEE__EED_Single_Page_Checkout___initialize__checkout   filter $checkout   After checkout object is created/restored from session
FHEE__EED_Single_Page_Checkout__init___continue_reg   filter bool  , $checkout   Before reg steps are loaded
AHEE__Single_Page_Checkout___load_and_instantiate_reg_steps__start   action $checkout   Before reg step classes are instantiated
FHEE__Single_Page_Checkout__load_reg_steps__reg_steps   filter $reg_steps   After reg step objects are created
AHEE__Single_Page_Checkout___initialize__after_final_verifications   action $checkout   After all verifications pass; transaction is locked
AHEE__Single_Page_Checkout___initialize_reg_step__{SLUG}   action $reg_step   Per-step initialization (access checkout via $reg_step->checkout  )

Form Action Phase

Hook Type Params When
AHEE__Single_Page_Checkout__before_{SLUG}__{CURRENT_ACTION}   action $current_step   Before the step's action method is called
AHEE__Single_Page_Checkout__process_reg_step__{SLUG}__{CURRENT_ACTION}   filter (despite prefix) bool  , $current_step  , $spco   Only fires in the default branch (not display_spco_reg_step  ); return false   to skip calling the method. Use add_filter()   to control execution; add_action()   is fine for observers.
AHEE__Single_Page_Checkout__after_{SLUG}__{CURRENT_ACTION}   action $current_step   Fires after the action dispatch (even if method is missing or filter blocks execution)

Step-Specific Hooks

Attendee Information step:


Hook Type Params When
AHEE__EE_Single_Page_Checkout__process_attendee_information__end   action $reg_step  , $valid_data   After all attendee data is saved to registrations
FHEE__EE_SPCO_Reg_Step_Attendee_Information___process_registrations__pre_registration_process   filter bool  , $attendee_count  , $registration  , $registrations  , $reg_form_data  , $reg_step   Per-registration, before processing; return true   to skip processing this registration
FHEE__EventEspresso_core_domain_services_registration_form_v1_RegFormInputHandler__saveRegistrationFormInput   (replaces legacy filter: FHEE__EE_SPCO_Reg_Step_Attendee_Information___save_registration_form_input  ) filter bool  , $registration  , $form_input  , $input_value  , $reg_step   Per-input; return true   to bypass default save logic (your plugin handles saving); return false   (default) to allow normal save processing

Filter polarity reference (both filters use the same convention):


Filter Return true   Return false   (default)
__pre_registration_process   Skip processing this registration Process normally
__saveRegistrationFormInput   Bypass default save (plugin handles it) Save through standard processing

Note: The __save_registration_form_input   legacy filter is bridged to the modern

__saveRegistrationFormInput  

filter via setLegacyFiltersForRegFormProcessing()  . The legacy filter is added for backward compatibility, but the modern replacement is preferred; .


Payment Options step:


Hook Type Params When
AHEE__EE_SPCO_Reg_Step_Payment_Options__generate_reg_form__registrations_requiring_payment   action $reg_step  , $registrations   When building the payment form; provides list of registrations that owe money
AHEE__EE_Single_Page_Checkout__process_finalize_registration__before_gateway   action $transaction   Just before payment processing on revisit/retry (in update_reg_step()  )

Finalize Registration step:


Hook Type Params When
AHEE__EE_SPCO_Reg_Step_Finalize_Registration__process_reg_step__completed   action $checkout  , $txn_update_params   After finalization is fully complete (transaction updated, notifications triggered)

3.2 Payment Processing Hooks

Hook Type Params Location When
AHEE__EE_Payment_Processor__update_txn_based_on_payment__successful   action $transaction  , $payment   PaymentProcessor.php   → updateTransactionBasedOnPayment()   (resolved in processRegistrationPayments()  ) Payment approved (not necessarily final business outcome; refunds/voids can follow; paid-in-full depends on remaining balance)
AHEE__EE_Payment_Processor__update_txn_based_on_payment__not_successful   action $transaction  , $payment   PaymentProcessor.php   → updateTransactionBasedOnPayment()   Payment exists but not approved
AHEE__EE_Payment_Processor__update_txn_based_on_payment__no_payment_made   action $transaction  , $payment   PaymentProcessor.php   → processRegistrationPayments()   No payment saved
AHEE__EE_Payment_Processor__update_txn_based_on_payment   action $transaction  , $payment   PaymentProcessor.php   → updateTransactionBasedOnPayment()   After any payment update (fires on every call)

3.3 Transaction & Registration Status Hooks

Hook Type Params Location When
AHEE__EE_Transaction_Processor__update_transaction_and_registrations_after_checkout_or_payment   action $transaction  , $update_params   EE_Transaction_Processor.class.php   → update_transaction_and_registrations_after_checkout_or_payment()   After TXN + registrations are finalized
AHEE__EE_Registration_Processor__trigger_registration_update_notifications   action $registration  , $additional_details   EE_Registration_Processor.class.php   → trigger_registration_update_notifications()   When notification emails should fire (primary registrant only)
AHEE__EE_Registration__set_status__after_update   action $registration  , $old_STS_ID  , $new_STS_ID  , $context   EE_Registration.class.php   → set_status()   After any registration status change (fires last, after all sub-hooks below)
AHEE__EE_Registration__set_status__to_approved   action $registration  , $old_STS_ID  , $new_STS_ID  , $context   EE_Registration.class.php   → set_status()   When a registration becomes approved
AHEE__EE_Registration__set_status__from_approved   action $registration  , $old_STS_ID  , $new_STS_ID  , $context   EE_Registration.class.php   → set_status()   When a registration leaves approved status
AHEE__EE_Registration__set_status__canceled_or_declined   action $registration  , $old_STS_ID  , $new_STS_ID  , $context   EE_Registration.class.php   → updateIfCanceled()   When a non-closed registration is cancelled or declined
AHEE__EE_Registration__set_status__after_reinstated   action $registration  , $old_STS_ID  , $new_STS_ID  , $context   EE_Registration.class.php   → updateIfReinstated()   When a cancelled/declined registration is reinstated

Firing conditions:


Please note that at least one of the AHEE__EE_Registration__set_status__*   hooks will fire any time a registration status is updated, and not just during registration checkout.


This includes status changes applied manually by an event manager, admin dashboard update, IPN, programmatic update, or if a registrant cancels their registration at a later date. If you need to catch every transition, use __after_update  .


Hook Fires when
__after_update   Every status change (guaranteed)
__to_approved   New status is APPROVED  
__from_approved   Old status was APPROVED   (mutually exclusive with __to_approved  )
__canceled_or_declined   Transitioning into a closed status (cancelled/declined) from a non-closed status
__after_reinstated   Transitioning out of a closed status (cancelled/declined) to a non-closed status

Only __after_update   fires on every status change. The others are conditional.


The $update_params   array for the transaction processor hook contains:


[
    'old_txn_status'   => string,         // Status before this update
    'new_txn_status'   => string,         // Current status after update
    'finalized'        => bool|int,       // Whether finalize step is completed
    'revisit'          => bool,           // Whether this is a return visit
    'payment_updates'  => bool,           // Whether a payment was involved
    'last_payment'     => EE_Payment|null,// The payment object
    'status_updates'   => bool,           // Whether any registration statuses changed
]

3.4 Thank-You Page Hooks

Hook Type Params Location
AHEE__EED_Thank_You_Page__init_end   action $transaction   EED_Thank_You_Page.module.php   → init()  
AHEE__thank_you_page_overview_template__top   action $transaction   thank-you-page-overview.template.php   (top of template)
AHEE__thank_you_page_overview_template__content   action $transaction   thank-you-page-overview.template.php   (content area)
AHEE__thank_you_page_overview_template__bottom   action $transaction   thank-you-page-overview.template.php   (bottom of template)
AHEE__thank_you_page_registration_details_template__after_event_name   action $event  , $registration   thank-you-page-registration-details.template.php   (after event name)
AHEE__thank_you_page_registration_details_template__after_registration_table_row   action $registration   thank-you-page-registration-details.template.php   (after table row)


4. Concrete Examples

4.1 Capture Attendee Data at Registration Step (Pre-Payment)

Use this when you need to create a CRM contact as soon as the attendee submits their info, before they pay. This enables follow-up automation if they abandon checkout.


add_action(
    'AHEE__Single_Page_Checkout__after_attendee_information__process_reg_step',
    'my_crm_capture_lead_at_registration',
    10,
    1
);

/**
 * Fires after the attendee_information step is processed.
 *
 * @param EE_SPCO_Reg_Step_Attendee_Information $reg_step
 */
function my_crm_capture_lead_at_registration(EE_SPCO_Reg_Step_Attendee_Information $reg_step)
{
    $checkout = $reg_step->checkout;

    // Skip if step didn't complete successfully
    if (! $reg_step->completed()) {
        return;
    }

    $transaction = $checkout->transaction;

    $registration = $transaction->primary_registration();
    if (! $registration instanceof EE_Registration) {
        return;
    }
    // If you need all attendees, loop $transaction->registrations() instead.

    /** @var EE_Registration $registration */
    $attendee = $registration->attendee();

    if (! $attendee instanceof EE_Attendee) {
        return;
    }

    // Attendee data is now available
    $contact_data = [
        'first_name' => $attendee->fname(),
        'last_name'  => $attendee->lname(),
        'email'      => $attendee->email(),
        'phone'      => $attendee->phone(),
        'address'    => $attendee->address(),
        'city'       => $attendee->city(),
        'state'      => $attendee->state_name(),
        'zip'        => $attendee->zip(),
        'country'    => $attendee->country_name(),
    ];

    // Event and ticket context
    $event_name  = $registration->event_name();
    $ticket_name = $registration->ticket()->name();

    // Custom question answers
    $answers = [];
    foreach ($registration->answers() as $answer) {
        /** @var EE_Answer $answer */
        $question = $answer->question();
        if ($question instanceof EE_Question) {
            $value = $answer->value();
            $answers[$question->admin_label()] = is_array($value) ? implode(', ', $value) : $value;
        }
    }

    // Transaction context (payment NOT yet made)
    $txn_total = $transaction->total();
    $txn_id    = $transaction->ID();
    // Optional correlation key for your CRM
    // $crm_key = "txn:$txn_id";

    // Send to your CRM
    // my_crm_create_lead($contact_data, $event_name, $ticket_name, $answers, $txn_id, $txn_total);
}

Handling group registrations (multiple attendees per transaction):


foreach ($transaction->registrations() as $registration) {
    /** @var EE_Registration $registration */
    $attendee = $registration->attendee();
    if (! $attendee instanceof EE_Attendee) {
        continue;
    }

    $contact_data = [
        'first_name' => $attendee->fname(),
        'last_name'  => $attendee->lname(),
        'email'      => $attendee->email(),
        'is_primary' => $registration->is_primary_registrant(),
        'event_name' => $registration->event_name(),
        'ticket'     => $registration->ticket()->name(),
    ];

    // my_crm_create_lead($contact_data, $txn_id);
}

You can also use the more specific hook that fires at the very end of process_reg_step()   inside the attendee information step class itself.


Class prefix note: This hook uses EE_Single_Page_Checkout   (the legacy class prefix), not

Single_Page_Checkout   (used by the dynamic SPCO controller hooks above). The difference is because this

hook originates inside EE_SPCO_Reg_Step_Attendee_Information  , not EED_Single_Page_Checkout  .


add_action(
    'AHEE__EE_Single_Page_Checkout__process_attendee_information__end',
    'my_crm_capture_lead_specific',
    10,
    2
);

/**
 * @param EE_SPCO_Reg_Step_Attendee_Information $reg_step
 * @param array $valid_data  The validated form data array
 */
function my_crm_capture_lead_specific(
    EE_SPCO_Reg_Step_Attendee_Information $reg_step,
    array $valid_data
) {
    $checkout = $reg_step->checkout;
    $transaction = $checkout->transaction;
    // ... same data access as above
}

4.2 Capture Data After SPCO Finalization

Use this when you need order data after SPCO finalization; registrations/status may be updated, and off-site payment confirmation may arrive later via IPN.

In CRMs: create the order once per txn_id, then update it on later status/payment changes.


add_action(
    'AHEE__EE_SPCO_Reg_Step_Finalize_Registration__process_reg_step__completed',
    'my_crm_create_order_after_payment',
    10,
    2
);

/**
 * Fires after the finalize_registration step completes.
 *
 * @param EE_Checkout $checkout
 * @param array       $txn_update_params
 */
function my_crm_create_order_after_payment(EE_Checkout $checkout, array $txn_update_params)
{
    $transaction = $checkout->transaction;
    // WARNING: $checkout->payment can be null for free events AND for off-site gateways
    // where the IPN has not yet arrived. Always null-check before accessing properties.
    $payment     = $checkout->payment;

    // Transaction details
    $order_data = [
        'txn_id'     => $transaction->ID(),
        'total'      => $transaction->total(),
        'paid'       => $transaction->paid(),
        'status'     => $transaction->status_ID(),
        'is_revisit' => $txn_update_params['revisit'] ?? false,
    ];

    // Payment details (if a payment was made)
    if ($payment instanceof EE_Payment) {
        $payment_method = $payment->payment_method(); // may be null depending on gateway/load path
        $order_data['payment_amount']  = $payment->amount();
        if ($payment_method instanceof EE_Payment_Method) {
            $order_data['payment_method'] = $payment_method->name();
        } else {
            $order_data['payment_method'] = 'unknown';
        }
        $order_data['payment_status']  = $payment->status();
        // Avoid sending raw gateway_response to a CRM; store only safe subsets (payment ID, reference, approval code, last4, timestamp).
    }

    // All registrations with their final statuses
    foreach ($transaction->registrations() as $registration) {
        /** @var EE_Registration $registration */
        $attendee = $registration->attendee();
        if (! $attendee instanceof EE_Attendee) {
            continue;
        }

        $reg_data = [
            'reg_id'     => $registration->ID(),
            'reg_code'   => $registration->reg_code(),
            'status'     => $registration->status_ID(), // e.g., RegStatus::APPROVED
            'event_name' => $registration->event_name(),
            'ticket'     => $registration->ticket()->name(),
            'price_paid' => $registration->final_price(),
            'attendee'   => [
                'name'  => $attendee->full_name(),
                'email' => $attendee->email(),
            ],
        ];

        // If you already created a CRM order for this txn_id, update it instead of creating a second.
        // my_crm_create_order_line($order_data, $reg_data);
    }
}

Alternative: Use the transaction processor hook for a broader trigger that also fires on IPN-based payment completions (off-site gateways):


add_action(
    'AHEE__EE_Transaction_Processor__update_transaction_and_registrations_after_checkout_or_payment',
    'my_crm_handle_transaction_update',
    10,
    2
);

/**
 * @param EE_Transaction $transaction
 * @param array          $update_params
 */
function my_crm_handle_transaction_update(EE_Transaction $transaction, array $update_params)
{
    // Only act on completed payments, not displays/initializations
    if (empty($update_params['payment_updates'])) {
        return;
    }

    // On revisit, do NOT create a new CRM order. Update existing CRM record or skip create. Don't create duplicates.
    if (! empty($update_params['revisit'])) {
        // my_crm_update_order($transaction, $update_params);
        // On revisit, update existing CRM order here and return.
        return;
    }

    $payment = $update_params['last_payment'] ?? null;

    // ... same data extraction as above via $transaction->registrations(), etc.
}

4.3 Capture Data on the Thank-You Page

Use this for client-side tracking pixels or additional server-side processing after the user has been redirected.


add_action(
    'AHEE__thank_you_page_overview_template__top',
    'my_crm_thank_you_page_tracking',
    10,
    1
);

/**
 * @param EE_Transaction $transaction
 */
function my_crm_thank_you_page_tracking(EE_Transaction $transaction)
{
    // Full transaction data is available, loaded fresh from DB
    $total        = $transaction->total();
    $paid         = $transaction->paid();
    $primary_reg  = $transaction->primary_registration();

    if (! $primary_reg instanceof EE_Registration) {
        return;
    }

    // Output a tracking pixel or JS snippet
    // Avoid sending raw PII; if you must, hash it first.
    // If you need browser-based revenue tracking, use txn id + amount only.
    printf(
        '<script>myTracker.purchase({txn: %d, total: %.2f});</script>',
        $transaction->ID(),
        $total
    );
}

For the server-side equivalent (before any template output):


add_action(
    'AHEE__EED_Thank_You_Page__init_end',
    'my_crm_thank_you_server_side',
    10,
    1
);

/**
 * @param EE_Transaction $transaction
 */
function my_crm_thank_you_server_side(EE_Transaction $transaction)
{
    // Fires during init, before template rendering
    // Good for API calls that don't need to output HTML
}



5. Decision Guide: Which Hook for Which CRM Use-Case

Lead Capture (Create Contact ASAP)

Goal: Create a CRM contact as soon as the attendee submits their name/email, regardless of whether they complete payment.


Use: AHEE__Single_Page_Checkout__after_attendee_information__process_reg_step  


Why:

  • Fires immediately after attendee info is saved
  • All contact fields (name, email, phone, address) are populated
  • Transaction and registration IDs are available for later linking
  • You can follow up on abandoned checkouts via CRM automation

Trade-off: Payment has not occurred. You must handle the case where the user never pays (update or delete the CRM record later).


Order Creation (After SPCO Finalization)

Goal: Create a CRM order/deal after the SPCO finalization step completes (registrations/status may be updated; off-site payment confirmation may arrive later via IPN).


Use: AHEE__EE_SPCO_Reg_Step_Finalize_Registration__process_reg_step__completed  


Why:

  • Payment step has run (on-site processed; off-site may still be awaiting IPN)
  • Registration statuses have been updated (approved, pending, etc.)
  • Transaction status reflects the payment outcome
  • Notification emails have been queued

Trade-off: For off-site gateways, the finalize step may not complete in the same request (the IPN arrives later). Use the transaction processor hook as a fallback.


Payment-Aware Processing (Handles Both On-Site and IPN)

Goal: React to any payment update, including asynchronous IPN callbacks from off-site gateways.


Use: AHEE__EE_Payment_Processor__update_txn_based_on_payment  


Why:

  • Fires on every payment-related transaction update
  • Works for on-site, off-site, and offline payment methods
  • Receives both the transaction and payment objects
  • Payment approved does not necessarily mean paid-in-full; check remaining balance/txn status

Trade-off: May fire multiple times for the same transaction (initial payment + IPN). Guard against duplicate processing.


Registration Status Changes (Fine-Grained Control)

Goal: React whenever a specific registration's status changes (e.g., approved, cancelled, declined).


Use: AHEE__EE_Registration__set_status__after_update   or the more specific AHEE__EE_Registration__set_status__to_approved  


Why:

  • Fires for every status change, regardless of what triggered it
  • Provides both old and new status IDs
  • Works for admin-initiated changes, payment updates, and checkout flow

Trade-off: Fires per-registration, not per-transaction. If you need transaction-level context, retrieve it from $registration->transaction()  .


Thank-You Page Tracking (Client-Side Pixels / Conversion Tracking)

Goal: Fire a conversion pixel or client-side analytics event when the user sees the confirmation page.


Use: AHEE__thank_you_page_overview_template__top  


Why:

  • The user is present in the browser (not a server-side callback)
  • Transaction is loaded fresh from the database
  • You can output <script>   tags directly

Trade-off: Only fires if the user actually reaches the Thank-You page. Off-site gateway returns and abandoned sessions may never trigger this.


Advanced: Modifying Instantiated Step Objects

For advanced use cases (e.g., conditionally removing or reordering steps), use the post-instantiation filter:


Use: FHEE__Single_Page_Checkout__load_reg_steps__reg_steps  


This filter receives the array of instantiated EE_SPCO_Reg_Step   objects after all steps have been created. Unlike

AHEE__SPCO__load_reg_steps__reg_steps_to_load   (which filters raw step definitions before instantiation), this

filter lets you inspect fully constructed step objects — useful for conditional logic based on step state or

configuration.


Quick Reference Table

Use Case Hook Data Available
Lead capture (pre-payment) AHEE__Single_Page_Checkout__after_attendee_information__process_reg_step   Attendee, Registration, Transaction (no payment)
Order creation (after SPCO finalization) AHEE__EE_SPCO_Reg_Step_Finalize_Registration__process_reg_step__completed   Transaction + registrations (status may change later; may precede off-site IPN)
Payment processing (all gateways) AHEE__EE_Payment_Processor__update_txn_based_on_payment   Transaction, Payment
Transaction finalization (all paths) AHEE__EE_Transaction_Processor__update_transaction_and_registrations_after_checkout_or_payment   Transaction, update_params array
Registration approved AHEE__EE_Registration__set_status__to_approved   Registration, old/new status
Any status change AHEE__EE_Registration__set_status__after_update   Registration, old/new status, context
Conversion pixel / JS tracking AHEE__thank_you_page_overview_template__top   Transaction (user is in browser)


6. Production Notes

6.1 Reliability and Performance

  • Do not block checkout on CRM failures. Queue or async-send, and keep API timeouts short.
  • Log only IDs/status; avoid logging raw emails/addresses in plaintext.
  • Capture consent fields (if present) and send them with your CRM payload.
  • Decide how you want to handle refunds/chargebacks (txn/payment hooks) and cancellations (reg status hooks).

6.2 Testing Your Hooks

To verify a hook is firing and inspect what data is available:


Quick debug callback:


add_action('AHEE__Single_Page_Checkout__after_attendee_information__process_reg_step', function ($reg_step) {
    error_log('Hook fired: after_attendee_information__process_reg_step');
    error_log('Step completed: ' . ($reg_step->completed() ? 'yes' : 'no'));
    error_log('Transaction ID: ' . $reg_step->checkout->transaction->ID());
}, 10, 1);

Verify a hook has fired (useful in later hooks):


// did_action() returns the number of times the action has fired
if (did_action('AHEE__EE_Payment_Processor__update_txn_based_on_payment__successful')) {
    // payment was confirmed at some point during this request
}

Check if a filter has listeners:


if (has_filter('FHEE__EE_SPCO_Reg_Step_Attendee_Information___save_registration_form_input')) {
    // another plugin is filtering form input saves
}

6.3 Idempotency Guard (Duplicate Prevention)

Payments and status changes can fire multiple times (IPN + retries + admin adjustments). Use an idempotency key

or a transaction meta flag to avoid duplicate CRM records.


// PSEUDO-CODE (EE extra-meta API): store a durable "already sent" flag keyed by txn + integration event.
$meta_key = 'crm_sent_example_order';
if ($transaction->get_extra_meta($meta_key, true)) {
    return;
}
$transaction->update_extra_meta($meta_key, time());

Note: Examples use the EE_Base_Class   extra-meta API, but you can store this flag elsewhere (custom table, options, etc.).

Fallback: store {integration}:{event}:{txn_id}   in an option or custom table; options are fine for low volume but not ideal at scale.

  • Best practice: Create the CRM order once keyed to txn_id  , then update it when last_payment   or transaction/registrationstatus changes occur.

Example keys:

  • order_key = "ee:txn:{txn_id}"   (create/update the same CRM order).
  • event_key = "ee:event:{type}:txn:{txn_id}:pay:{payment_id|none}"   (idempotency per event). If payment_id   ismissing, use none:{reason}   (free event, no_payment_made, retry), and if you need monotonic uniqueness, appendts:{payment_timestamp_gmt}   or status:{status}:amount:{amount}  .



7. Common Pitfalls

7.1 Dynamic Hook Name Mismatch

The most common mistake is constructing the wrong hook name. The dynamic pattern is:


AHEE__Single_Page_Checkout__before_{SLUG}__{ACTION}
AHEE__Single_Page_Checkout__after_{SLUG}__{ACTION}

The process_reg_step__{SLUG}__{ACTION}   filter hook exists too; see Section 2 for that pattern.


Where the prefix is before   or after  . Note the underscore placement:

  • Between before  /after   and the slug: single underscore
  • Between the slug and the action: double underscore

Correct:

'AHEE__Single_Page_Checkout__after_attendee_information__process_reg_step'
//                          ^^     ^                    ^^
//                     separator  single            separator

Wrong (extra underscore before slug):

'AHEE__Single_Page_Checkout__after__attendee_information__process_reg_step'
//                                ^^
//                         This extra underscore breaks the hook name

7.2 AHEE Prefix Used as Filter

Two hooks in the SPCO module use the AHEE__   prefix but are actually called via apply_filters()  :


  • AHEE__SPCO__load_reg_steps__reg_steps_to_load   (filter, not action)
  • AHEE__Single_Page_Checkout__process_reg_step__{SLUG}__{ACTION}   (filter, not action)

If you use add_action()   on these, your callback will fire (WordPress treats them identically), but if you need to return a value to control behavior, use add_filter()  .


7.3 Off-Site Gateways and IPN Timing

For off-site payment gateways (e.g., PayPal Standard), the checkout flow is:


  1. User submits payment options form
  2. User is redirected to the gateway
  3. finalize_registration   step may attempt to run but may not mark itself as completed
  4. User returns to the Thank-You page
  5. The gateway sends an IPN to your server asynchronously

Impact: AHEE__EE_SPCO_Reg_Step_Finalize_Registration__process_reg_step__completed   fires in step 3, but the payment may not be confirmed yet. The AHEE__EE_Payment_Processor__update_txn_based_on_payment__successful   hook fires when the IPN arrives in step 5.


Recommendation: For payment-dependent CRM updates, use:

'AHEE__EE_Payment_Processor__update_txn_based_on_payment__successful'

This covers both on-site and IPN-based payment confirmations.


7.4 Revisits and Duplicate Processing

Users can revisit SPCO to edit their information or retry a failed payment. The $checkout->revisit   property is true   in these cases. Most CRM integrations should guard against re-processing:


if ($checkout->revisit) {
    // Update existing CRM record instead of creating a new one
    // Or skip entirely
    return;
}

For the transaction processor hook, check $update_params['revisit']  .


7.5 Missing Attendee Object

The $registration->attendee()   method returns null   if no attendee has been linked yet. Always check:


$attendee = $registration->attendee();
if (! $attendee instanceof EE_Attendee) {
    return; // or continue to next registration
}

This typically happens when hooking into steps before attendee_information   completes, or for registrations that were created but not yet processed.


7.6 Free Event Behavior

If all tickets are free (or no payment is required), the checkout flow differs in several ways:


  • The payment_options   step still runs but renders the no-payment-required UI and can auto-complete without apayment attempt. Hooks targeting AHEE__Single_Page_Checkout__after_payment_options__process_reg_step   fire,but there will be no EE_Payment   object.
  • $checkout->payment   is null   throughout the flow. Any code accessing payment properties must null-check.
  • The finalize_registration   step completes and firesAHEE__EE_SPCO_Reg_Step_Finalize_Registration__process_reg_step__completed   normally.
  • The transaction status may be set to EEM_Transaction::complete_status_code   without any payment record.
  • Payment processor hooks (AHEE__EE_Payment_Processor__update_txn_based_on_payment*  ) do not fire for freeevents since no payment processing occurs.
  • Registration statuses are still updated (typically to approved), soAHEE__EE_Registration__set_status__to_approved   fires as expected.

Recommendation: If your integration must handle both free and paid events, hook into the finalize step or

registration status hooks rather than payment hooks.


7.7 Step Slug Variability from Add-ons

Third-party add-ons can register additional reg steps via the AHEE__SPCO__load_reg_steps__reg_steps_to_load   filter. If your integration depends on step order (e.g., "attendee_information is always followed by payment_options"), it may break when custom steps are inserted between them.


7.8 Transaction Lock

The SPCO module locks the transaction during processing (via lockTransaction()  ) to prevent concurrent modifications. If you're making direct DB changes within a hook, be aware that another request for the same transaction may be blocked until the lock is released at go_to_next_step()  .


7.9 Concurrent Sessions and Race Conditions

Several real-world scenarios can cause hooks to fire concurrently or in unexpected order for the same transaction:


  • Multiple browser tabs: A user opens SPCO in two tabs and submits both. The transaction lock (7.8) serializesDB writes, but your CRM callback may still receive near-simultaneous invocations. Use the idempotency guard(Section 6.3) to prevent duplicate records.
  • IPN arriving during finalize: For off-site gateways, the IPN webhook can arrive while the user is still onthe finalize step (or even before they return from the gateway). This meansAHEE__EE_Payment_Processor__update_txn_based_on_payment__successful   may fire in a separate PHP process fromAHEE__EE_SPCO_Reg_Step_Finalize_Registration__process_reg_step__completed  . Do not assume these fire in thesame request.
  • Admin actions during checkout: An admin can change registration statuses while the attendee is mid-checkout,firing AHEE__EE_Registration__set_status__after_update   independently of the SPCO flow.

Recommendation: Design all hook callbacks to be idempotent and independent of firing order. Key CRM records by

txn_id   and use create-or-update semantics rather than assuming a single linear flow.




8. Code Location References

All paths are relative to plugins/event-espresso-core/  .


Core SPCO Files

File Purpose
modules/single_page_checkout/EED_Single_Page_Checkout.module.php   Main SPCO controller; dynamic hook dispatch, AJAX handlers, initialization
modules/single_page_checkout/inc/EE_Checkout.class.php   Checkout state object; holds all step, action, transaction, payment references
modules/single_page_checkout/inc/EE_SPCO_Reg_Step.class.php   Abstract base class for all registration steps

Registration Step Classes

File Slug Purpose
modules/single_page_checkout/reg_steps/attendee_information/EE_SPCO_Reg_Step_Attendee_Information.class.php   attendee_information   Collects attendee data, saves to EE_Attendee
modules/single_page_checkout/reg_steps/payment_options/EE_SPCO_Reg_Step_Payment_Options.class.php   payment_options   Displays payment methods, processes payment
modules/single_page_checkout/reg_steps/finalize_registration/EE_SPCO_Reg_Step_Finalize_Registration.class.php   finalize_registration   Finalizes transaction, triggers notifications

Payment & Transaction Processing

File Purpose
core/services/payments/PaymentProcessor.php   Modern payment processor; fires payment success/failure hooks
core/EE_Payment_Processor.core.php   Legacy payment processor singleton
core/services/payments/RegistrationPayments.php   Applies payments to individual registrations
core/business/EE_Transaction_Processor.class.php   Transaction status updates, registration status toggling
core/business/EE_Registration_Processor.class.php   Registration status management, notification triggers

Entity Classes (Data Objects)

File Class Key Methods
core/db_classes/EE_Transaction.class.php   EE_Transaction   total()  , paid()  , remaining()  , status_ID()  , registrations()  , primary_registration()  , reg_steps()  
core/db_classes/EE_Registration.class.php   EE_Registration   attendee()  , event()  , ticket()  , answers()  , status_ID()  , reg_code()  , final_price()  , is_primary_registrant()  
core/db_classes/EE_Attendee.class.php   EE_Attendee   fname()  , lname()  , email()  , phone()  , address()  , city()  , state_name()  , zip()  , country_name()  
core/db_classes/EE_Payment.class.php   EE_Payment   amount()  , status()  , payment_method()  , gateway_response()  , just_approved()  

Thank-You Page

File Purpose
modules/thank_you_page/EED_Thank_You_Page.module.php   Thank-You page controller; AJAX polling for IPN
modules/thank_you_page/templates/thank-you-page-overview.template.php   Main template with __top  , __content  , __bottom   hooks
modules/thank_you_page/templates/thank-you-page-registration-details.template.php   Registration details template
modules/thank_you_page/templates/thank-you-page-transaction-details.template.php   Transaction details template
modules/thank_you_page/templates/thank-you-page-payment-details.template.php   Payment details template

Key Method Locations

Method File Purpose
_get_request_vars()   EED_Single_Page_Checkout.module.php   Sets checkout->step  , checkout->action   from request
_process_form_action()   EED_Single_Page_Checkout.module.php   Dispatches to current step's action method; fires dynamic hooks
_initialize_reg_steps()   EED_Single_Page_Checkout.module.php   Calls each step's initialize_reg_step()   and fires per-step init hooks
set_current_step()   EE_Checkout.class.php   Resolves step slug to step object
process_reg_step()   (attendee) EE_SPCO_Reg_Step_Attendee_Information.class.php   Saves attendee data
process_reg_step()   (payment) EE_SPCO_Reg_Step_Payment_Options.class.php   Processes payment
process_reg_step()   (finalize) EE_SPCO_Reg_Step_Finalize_Registration.class.php   Finalizes transaction
updateTransactionBasedOnPayment()   PaymentProcessor.php   Updates TXN after payment; fires payment hooks
update_transaction_and_registrations_after_checkout_or_payment()   EE_Transaction_Processor.class.php   Central TXN finalization method


9. Hook Firing Flowchart

The diagram below shows which hooks fire in each checkout path. Follow the arrows for your scenario.


                    ┌───────────────────────────────┐
                    │       Ticket Selection        │
                    │   (tickets added to cart)     │
                    └───────────────┬───────────────┘
                                    │
                                    ▼
                    ┌───────────────────────────────┐
                    │      SPCO initialization      │
                    │       (Transaction and        │
                    │     Registrations created)    │
                    └───────────────┬───────────────┘
                                    │
                    ┌───────────────────────────────┐
                    │  Step 1: attendee_information │
                    │        process_reg_step       │
                    └───────────────┬───────────────┘
                                    │
                         Hooks that fire:
                         • before_attendee_information__process_reg_step
                         • process_reg_step__attendee_information__process_reg_step (filter)
                         • after_attendee_information__process_reg_step  ◄── LEAD CAPTURE
                         • process_attendee_information__end
                                    │
                                    ▼
                    ┌───────────────────────────────┐
                    │    Step 2: payment_options    │
                    │        process_reg_step       │
                    └───────────┬───────────────────┘
                                │
                    ┌───────────┴───────────┐
                    │                       │
                    ▼                       ▼
            ┌──────────────┐       ┌───────────────────┐
            │  FREE EVENT  │       │    PAID EVENT     │
            │  (no payment │       └────────┬──────────┘
            │   object)    │       ┌────────┴──────────┐
            └──────┬───────┘       │                   │
                   │               ▼                   ▼
                   │    ┌───────────────────┐ ┌──────────────────┐
                   │    │ On-site gateway   │ │ Off-site gateway │
                   │    │    no redirect    │ │     redirect     │
                   │    │ immediate payment │ │ deferred payment │
                   │    └─────────┬─────────┘ └─────┬────────────┘
                   │              │                 │
                   │              ▼                 │
                   │     ┌──────────────────┐       │
                   │     │ PaymentProcessor │       │
                   │     │    successful    │       │
                   │     └────────┬─────────┘       │
                   │              │                 │
                   ▼              ▼                 ▼
            ┌─────────────────────────────────────────────┐
            │      Step 3: finalize_registration          │
            │            process_reg_step                 │
            └───────────────────┬─────────────────────────┘
                                │
                     Hooks that fire:
                     • finalize_registration__process_reg_step__completed  ◄── ORDER CREATION
                     • update_transaction_and_registrations...
                     • EE_Registration__set_status__to_approved
                     • EE_Registration__set_status__after_update
                                │
                                ▼
                    ┌───────────────────────┐
                    │    Thank-You Page     │
                    │                       │
                    │ • thank_you_page_     │
                    │   overview_template   │  ◄── CONVERSION PIXEL
                    │   __top               │
                    └───────────────────────┘

                     ═══════════════════════════
                     ASYNC (off-site only):
                     ═══════════════════════════

                    ┌───────────────────────┐
                    │  IPN / Webhook        │
                    │  (separate request)   │
                    └───────────┬───────────┘
                                │
                     Hooks that fire:
                     • PaymentProcessor__update_txn_based_on_payment__successful  ◄── PAYMENT CONFIRMED
                     • PaymentProcessor__update_txn_based_on_payment (global)
                     • update_transaction_and_registrations...
                     • EE_Registration__set_status__after_update (if status changes)

Key decision points:

  • Need data before payment? Hook into Step 1 (after_attendee_information  ).
  • Need data after SPCO finishes? Hook into Step 3 (finalize_registration__completed  ).
  • Need confirmed payment (all gateways)? Hook into PaymentProcessor__successful   — fires in Step 3 for on-site,or asynchronously via IPN for off-site.
  • Need browser context for pixels? Hook into the Thank-You page template.



Generated from code analysis of Event Espresso Core in this repo at the time of writing.

Did this answer your question? Thanks for the feedback There was a problem submitting your feedback. Please try again later.