Developer guide
A documented, stable extension API (version 1.0) for themes, plugins and agencies. Build against what's listed here; anything else is internal and may change.
Architecture
- Free boots on
plugins_loadedonce its requirements are met, then firessiteryn_abandoned_cart_loaded. Extensions, including Pro, start there. - Free never depends on Pro: every hook fires whether or not Pro is installed.
- Cart records live in their own table, accessed only through
CartRepository(the unsubscribe list has a second table). Status changes go throughCartRepository::transition(), which validates, stamps the time, records the event, saves and fires the hooks. - Background work runs on the five-minute
siteryn_abandoned_cart_detectcron event: detection (priority 10), Pro campaigns (15), Free's sender (20), Pro webhook deliveries (30). - A recovery link only rebuilds the cart. The cart moves to
recoveredwhen the landing page's beacon confirms a real browser, or when an order is placed from the restored session, so email security scanners never count as recoveries. - There are no REST routes. Storefront requests use a nonce-checked, rate-limited admin-ajax router (
admin-ajax.php?action=siteryn_abandoned_cart_{name}). Free registers three endpoints:capture_email(20 per minute),consent(20) andconfirm_recovery(10, the recovery-link confirmation beacon). All guests sharing one IP may use 10 times an endpoint's limit together. Pro's optional open-tracking image is served oninitfor URLs carrying a signedsiteryn-acart-openquery argument.
Public functions
Loaded only after requirements are met, so wrap calls in function_exists().
siteryn_abandoned_cart_get_setting( string $key ): mixed|null // null for an unknown key siteryn_abandoned_cart_api_version(): string // '1.0' siteryn_abandoned_cart_get_cart( int $cart_id ): ?Cart siteryn_abandoned_cart_get_current_cart(): ?Cart // the visitor's open cart siteryn_abandoned_cart_create_recovery_url( Cart $cart ): string // issues a NEW token; '' if the cart is not open siteryn_abandoned_cart_get_metrics( int $days = 30 ): array // 7, 30, 90 or 0 (all time) siteryn_abandoned_cart_is_pro_active(): bool // Pro siteryn_abandoned_cart_pro_api_version(): string // '1.0'
if ( function_exists( 'siteryn_abandoned_cart_get_setting' ) ) {
$minutes = siteryn_abandoned_cart_get_setting( 'abandonment_timeout' ); // 60
}
Data model
The Cart object
Public properties of Siteryn_Abandoned_Cart\Cart\Cart. Times are Unix integers, and 0 means "not yet".
| Property | Description |
|---|---|
id, customer_key, user_id | customer_key is u{user id} or g{session id}; user_id is 0 for guests |
email, first_name, locale | What the customer gave, and their language |
status | active · abandoned · recovery_sent · recovered · completed · expired |
items | Each {product_id, variation_id, variation, quantity, name, line_total, extra} |
total, currency, cart_hash | Total incl. tax and shipping |
order_id, order_total | The resulting order |
email_status, email_attempts | '', sent, failed or unsubscribed; attempts so far |
token_hash, token_expires_at | Hash of the current recovery token |
events | Timeline entries {t, type, note?}, max 50 |
created_at, updated_at, abandoned_at, recovery_sent_at, recovered_at, completed_at | Lifecycle timestamps |
Methods: is_open(), was_recovered(), item_count(), add_event( $type, $note = '' ).
CartRepository
find( int $id ): ?Cart query( array $args ): Cart[] count( array $args ): int sum( string $column, array $args ): float // 'total' or 'order_total' find_open_by_customer( string $key ): ?Cart find_by_token_hash( string $hash ): ?Cart find_by_order( int $order_id ): ?Cart save( Cart $cart ): bool delete( Cart $cart ): bool transition( Cart $cart, string $to, string $note = '' ): bool
Query arguments (combined with AND): ids, status (one or an array), customer_key, email, token_hash, currency, user_id, order_id, search (part of an email, or a cart ID), created_after/before, updated_after/before, abandoned_after/before, recovery_sent_after/before, completed_after/before (Unix times), email_attempts_below, recovered, anonymized, token_expired (bool), orderby (id, created_at, updated_at, abandoned_at, recovered_at, completed_at or total), order, limit (1–1000, default 20), offset.
use Siteryn_Abandoned_Cart\Cart\CartRepository; $carts = CartRepository::query( array( 'status' => array( 'abandoned', 'recovery_sent' ), 'orderby' => 'total', 'order' => 'DESC', 'limit' => 10, ) );
Database tables
| Table | Plugin | Contents |
|---|---|---|
{prefix}siteryn_abandoned_carts | Free | One row per cart. All datetimes are UTC. |
{prefix}siteryn_abandoned_cart_suppressions | Free | The unsubscribe list: email_hash (primary key), email_hint, source (link, one_click, admin), cart_id, created_at. Hashes use the per-site secret in siteryn_abandoned_cart_secret. |
{prefix}siteryn_abandoned_cart_pro_sequences | Pro | One campaign sequence per cart (unique cart_id) |
{prefix}siteryn_abandoned_cart_pro_events | Pro | Sends, clicks, opens, coupons, revenue, unsubscribes and webhook deliveries, with the A/B variant in data.variant. Emails stored only as HMAC hashes. |
Order meta: _siteryn_abandoned_cart_id (cart ID) and _siteryn_abandoned_cart_recovered (yes). Pro coupons carry _siteryn_abandoned_cart_pro.
Email templates
Copy to your-theme/siteryn-abandoned-cart/<path>. Each template declares an @version (currently 1.0.0), and outdated theme copies are flagged in the admin. Pro adds siteryn-abandoned-cart-pro/emails/admin-digest.php for the digest.
| Template | Variables in $args |
|---|---|
emails/recovery-email.php | cart, heading, message, button_text, footer (placeholders already replaced), url, items (each with image_url), total (HTML), total_text, logo_url, show_images, show_items, unsubscribe_url (empty when the link is off), unsubscribe_text, colors (base, base_text, text, background, body, header_bg, header_text) |
emails/plain/recovery-email.php | The same |
Add content without overriding the file using the siteryn_abandoned_cart_email_before_items and …_email_after_items actions.
wp-config constants
| Constant | Effect |
|---|---|
SITERYN_ABANDONED_CART_REMOVE_ALL_DATA | On uninstall, also removes _siteryn_abandoned_cart_id and _siteryn_abandoned_cart_recovered from orders |
SITERYN_ABANDONED_CART_PRO_AI_{PROVIDER}_API_KEY | AI key for one provider, e.g. …_OPENAI_API_KEY. Always wins, never stored. |
SITERYN_ABANDONED_CART_PRO_AI_API_KEY | One key for every built-in provider except Custom |
SITERYN_ABANDONED_CART_PRO_AI_{PROVIDER}_BASE_URL | Binds a constant key to an endpoint host |
SITERYN_FEEDBACK_ENDPOINT | Where the deactivation survey is sent (also filterable with siteryn_deactivation_feedback_endpoint) |
SCRIPT_DEBUG | Loads unminified CSS and JS |
Recipes
Keep add-on data when a cart is restored
add_filter( 'siteryn_abandoned_cart_item_data', function ( $extra, $cart_item ) {
if ( isset( $cart_item['gift_message'] ) && is_string( $cart_item['gift_message'] ) ) {
$extra['gift_message'] = sanitize_text_field( $cart_item['gift_message'] );
}
return $extra; // Passed back to WC_Cart::add_to_cart() as $cart_item_data on restore.
}, 10, 2 );
Add an order note for recovered purchases
add_action( 'siteryn_abandoned_cart_cart_completed', function ( $cart, $from ) {
if ( ! $cart->was_recovered() || ! $cart->order_id ) {
return;
}
$order = wc_get_order( $cart->order_id );
if ( $order ) {
$order->add_order_note( sprintf( 'Recovered from abandoned cart #%d.', $cart->id ) );
}
}, 10, 2 );
Add your own email placeholder
add_filter( 'siteryn_abandoned_cart_email_placeholders', function ( $placeholders, $cart ) {
$placeholders['{item_count}'] = (string) $cart->item_count();
return $placeholders;
}, 10, 2 );
Custom campaign condition Pro
add_filter( 'siteryn_abandoned_cart_pro_condition_types', function ( $types ) {
$types['item_count'] = array(
'label' => __( 'Items in cart', 'my-plugin' ),
'ops' => array( 'gte', 'lte' ),
'value' => 'integer', // number | integer | ids | slugs | choice:a,b
);
return $types;
} );
add_filter( 'siteryn_abandoned_cart_pro_condition_matches', function ( $result, $condition, $cart ) {
if ( 'item_count' !== $condition['type'] ) {
return $result;
}
$count = $cart->item_count();
return 'lte' === $condition['op'] ? $count <= (int) $condition['value'] : $count >= (int) $condition['value'];
}, 10, 3 );
Change the webhook payload Pro
add_filter( 'siteryn_abandoned_cart_pro_event_payload', function ( $payload, $event, $cart ) {
unset( $payload['cart']['first_name'] ); // Send less personal data.
$payload['cart']['locale'] = $cart->locale; // Add a field.
return $payload; // Never add recovery links or secrets.
}, 10, 3 );
The filter runs once, when the event is queued, not at each delivery attempt.
Custom integration Pro
Implement IntegrationInterface. Pro handles queueing, retries and the delivery log.
add_action( 'siteryn_abandoned_cart_pro_loaded', function () {
class My_CRM_Integration implements \Siteryn_Abandoned_Cart_Pro\Integrations\IntegrationInterface {
public function id() { return 'my_crm'; } // [a-z0-9_]{1,40}
public function label() { return 'My CRM'; }
public function is_enabled() { return defined( 'MY_CRM_API_KEY' ) && '' !== MY_CRM_API_KEY; }
public function events() { return array( 'cart.abandoned', 'cart.completed' ); }
public function deliver( $event, array $payload ) {
$response = wp_safe_remote_post( 'https://api.mycrm.example/v1/events', array(
'timeout' => 10,
'headers' => array( 'Authorization' => 'Bearer ' . MY_CRM_API_KEY, 'Content-Type' => 'application/json' ),
'body' => wp_json_encode( array( 'type' => $event, 'data' => $payload ) ),
) );
if ( is_wp_error( $response ) ) {
return new WP_Error( 'request_failed', $response->get_error_message(), array( 'status' => 0 ) );
}
$code = (int) wp_remote_retrieve_response_code( $response );
return ( $code >= 200 && $code < 300 ) ? true : new WP_Error( 'http_error', "HTTP {$code}", array( 'status' => $code ) );
}
}
add_filter( 'siteryn_abandoned_cart_pro_integrations', function ( $integrations ) {
$integrations[] = new My_CRM_Integration();
return $integrations;
} );
} );
Respect unsubscribes in your own emails
If you send cart-related emails yourself, check the unsubscribe list first:
use Siteryn_Abandoned_Cart\Recovery\Suppression;
if ( class_exists( Suppression::class ) && Suppression::is_suppressed( $email ) ) {
return; // The customer unsubscribed from cart reminders.
}
Other useful calls: Suppression::add( $email, $source, $cart_id ), Suppression::remove( $email ) (fires siteryn_abandoned_cart_resubscribed), Suppression::get( $email ), Suppression::suppressed_among( $emails ) and Unsubscribe::url( $cart ).
Send a recovery email from code
EmailSender::send() applies the same checks as the scheduled sender (bought elsewhere, unsubscribed, per-address cooldown, siteryn_abandoned_cart_should_send_email), issues a token, sends, moves the cart to recovery_sent and fires the hooks. It returns sent, failed or skipped.
use Siteryn_Abandoned_Cart\Recovery\EmailSender;
$cart = siteryn_abandoned_cart_get_cart( 42 );
if ( $cart ) {
$result = EmailSender::send( $cart, array(
'content' => array( 'subject' => 'Still thinking it over, {first_name}?' ),
'note' => 'Sent from my plugin', // Timeline note.
'context' => array( 'source' => 'my-plugin' ),
) );
}
Other options: token, store_token, expires_at, url, sender (from_name, from_address, reply_to), statuses (default active and abandoned; may add recovery_sent) and check_cooldown (default true).
Add a header to every recovery email
add_filter( 'siteryn_abandoned_cart_email_headers', function ( $headers, $cart, $to ) {
$headers[] = 'X-Campaign: cart-recovery';
return $headers;
}, 10, 3 );
Per-cart reply-to Pro
// $overrides may contain subject (already the A/B variant), heading, message,
// button_text, footer, from_name, from_address and reply_to.
add_filter( 'siteryn_abandoned_cart_pro_step_email', function ( $overrides, $cart, $campaign, $step ) {
$overrides['reply_to'] = 'help@your-store.com';
return $overrides;
}, 10, 4 );
Pro APIs Pro
Segments::all(),get( $id ),matches( $segment, Cart $cart ),count( array $conditions ). Custom condition types also appear in the Segments builder and live count. Keep matchers cheap, because a count checks up to 500 carts.TemplateLibrary::presets(),saved(),get( $id ),save( $name, array $step ),delete( $id ).SendWindow::is_open( time() )andSendWindow::next_allowed( $time ).ProMetrics::summary( $days )(totals, campaigns, products, categories andab_tests) andProMetrics::series( $days )(daily points). Both are cached for 10 minutes.WebhookIntegration::all(),find( $id ),rotate_secret( $id ).
Storefront JavaScript
Free exposes a small helper for its nonce-checked AJAX router:
// Resolves with the response data, rejects with { code, message }.
window.siterynAbandonedCart.request( 'capture_email', { email: 'sarah@example.com', first_name: 'Sarah' } )
.then( ( data ) => console.log( data.captured ) );
// Record a guest's consent choice: 'grant' or 'decline'.
window.siterynAbandonedCart.request( 'consent', { decision: 'decline' } )
.then( ( data ) => console.log( data.state ) ); // 'declined'
No custom DOM events are dispatched. See the full list of hooks in the Hooks reference.