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
- Quick Cheat Sheet
- When Data Becomes Available
- How Dynamic Hook Names Are Composed
- Hook Reference Map
- Concrete Examples
- Decision Guide: Which Hook for Which CRM Use-Case
- Production Notes
- Common Pitfalls
- Code Location References
- 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_loadfilter. If aregistration_confirmationstep 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_inputlegacy filter is bridged to the modern
__saveRegistrationFormInputfilter 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 thishook originates inside
EE_SPCO_Reg_Step_Attendee_Information, notEED_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 whenlast_paymentor 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). Ifpayment_idismissing, usenone:{reason}(free event, no_payment_made, retry), and if you need monotonic uniqueness, appendts:{payment_timestamp_gmt}orstatus:{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/afterand 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:
- User submits payment options form
- User is redirected to the gateway
finalize_registrationstep may attempt to run but may not mark itself as completed- User returns to the Thank-You page
- 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_optionsstep still runs but renders the no-payment-required UI and can auto-complete without apayment attempt. Hooks targetingAHEE__Single_Page_Checkout__after_payment_options__process_reg_stepfire,but there will be noEE_Paymentobject. $checkout->paymentisnullthroughout the flow. Any code accessing payment properties must null-check.- The
finalize_registrationstep completes and firesAHEE__EE_SPCO_Reg_Step_Finalize_Registration__process_reg_step__completednormally. - The transaction status may be set to
EEM_Transaction::complete_status_codewithout 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), so
AHEE__EE_Registration__set_status__to_approvedfires 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 means
AHEE__EE_Payment_Processor__update_txn_based_on_payment__successfulmay 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_updateindependently 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.