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.

FreeProAPI 1.0

Architecture

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".

PropertyDescription
id, customer_key, user_idcustomer_key is u{user id} or g{session id}; user_id is 0 for guests
email, first_name, localeWhat the customer gave, and their language
statusactive · abandoned · recovery_sent · recovered · completed · expired
itemsEach {product_id, variation_id, variation, quantity, name, line_total, extra}
total, currency, cart_hashTotal incl. tax and shipping
order_id, order_totalThe resulting order
email_status, email_attempts'', sent, failed or unsubscribed; attempts so far
token_hash, token_expires_atHash of the current recovery token
eventsTimeline entries {t, type, note?}, max 50
created_at, updated_at, abandoned_at, recovery_sent_at, recovered_at, completed_atLifecycle 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

TablePluginContents
{prefix}siteryn_abandoned_cartsFreeOne row per cart. All datetimes are UTC.
{prefix}siteryn_abandoned_cart_suppressionsFreeThe 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_sequencesProOne campaign sequence per cart (unique cart_id)
{prefix}siteryn_abandoned_cart_pro_eventsProSends, 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.

TemplateVariables in $args
emails/recovery-email.phpcart, 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.phpThe same

Add content without overriding the file using the siteryn_abandoned_cart_email_before_items and …_email_after_items actions.

wp-config constants

ConstantEffect
SITERYN_ABANDONED_CART_REMOVE_ALL_DATAOn uninstall, also removes _siteryn_abandoned_cart_id and _siteryn_abandoned_cart_recovered from orders
SITERYN_ABANDONED_CART_PRO_AI_{PROVIDER}_API_KEYAI key for one provider, e.g. …_OPENAI_API_KEY. Always wins, never stored.
SITERYN_ABANDONED_CART_PRO_AI_API_KEYOne key for every built-in provider except Custom
SITERYN_ABANDONED_CART_PRO_AI_{PROVIDER}_BASE_URLBinds a constant key to an endpoint host
SITERYN_FEEDBACK_ENDPOINTWhere the deactivation survey is sent (also filterable with siteryn_deactivation_feedback_endpoint)
SCRIPT_DEBUGLoads 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

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.