BEATSTORE

Docs / BeatStore WordPress Plugin

PHP Hooks

For themes and add-ons on the same WordPress install. These are not a second HTTP API. Catalog JSON stays on the REST API.

Filters that take extra arguments need the fourth add_filter argument set to the arity below (WordPress defaults to 1). Non-Offer items returned from beatstore_beat_offers are dropped. Core does not generate PDFs. BeatStore Contracts listens on the fulfillment hooks when the player is present, and on native EDD/Woo when it is not. That zip honors Freemius 38186 (7814 child) plus leftover standalone 13660 / 9802. A player key does not unlock PDFs.

HookTypeArityUse
beatstore_beat_offersfilter2License offers before REST / the player
beatstore_resolve_preview_urlfilter3Resolve or override a preview URL
beatstore_ai_crawler_patternsfilter1User-agent substrings blocked on protected preview paths
beatstore_preview_sourcesfilter1Register preview source modules
beatstore_preview_savedaction1After a beat preview is saved
beatstore_player_footeraction0Markup after the player
beatstore_checkout_before_submitaction1Echo fields before checkout submit
beatstore_checkout_required_fieldsfilter2Required checkout field names
beatstore_offer_cart_actionsaction3Extra markup per cart line
beatstore_contract_customerfilter2Buyer identity for a fulfilled line
beatstore_contract_producerfilter2Producer identity for a fulfilled line
beatstore_order_line_fulfilledaction3After a line is delivered
beatstore_sold_state_changedaction2Exclusive just marked sold
beatstore_register_modulesaction1Register feature modules
beatstore_commerce_adaptersaction1Register a commerce backend
beatstore_integrations_cardsaction0Cards on BeatStore → Integrations
beatstore_settings_afteraction0After BeatStore → Settings panels; #beatstore-settings-contracts is already in the wrap

beatstore_beat_offers

Filter license tiers on a beat after EDD/Woo builds them and before they become REST offers / the player license picker. Use this to hide a tier, rename it for the storefront, inject a complementary offer, or attach picker chips with $offer->with_includes().

apply_filters( 'beatstore_beat_offers', Offer[] $offers, int $post_id )
ArgumentTypeNotes
$offersBeatStore\Domain\Offer[]Value to return. Each item must stay an Offer instance.
$post_idintEDD download ID or Woo product ID

Return Offer[]. Non-arrays become []. Other types in the list are stripped.

Offer public properties: id (EDD price ID or Woo variation ID), name, amount (formatted string from the commerce plugin), exclusive, commerce_ref ({adapter}:{beatId}:{offerId} — keep this stable if you rewrite name), includes ({ kind: delivery|right, label, value? }[]). Core fills delivery chips from EDD/Woo file extensions. BeatStore Contracts merges right chips from the mapped template. Clone with $offer->with_includes( $chips ). Do not put contract HTML or file URLs on includes.

add_filter( 'beatstore_beat_offers', function ( $offers, $post_id ) {
	unset( $post_id );
	return array_values( array_filter(
		$offers,
		static fn( $offer ) => $offer instanceof \BeatStore\Domain\Offer && ! $offer->exclusive
	) );
}, 10, 2 );

beatstore_resolve_preview_url

Resolve an external permalink (SoundCloud, Dropbox share link, …) into a streamable audio URL, or override the stored preview. Runs while a beat is hydrated for REST and the player.

apply_filters( 'beatstore_resolve_preview_url', string $url, int $post_id, ?Preview $preview )
ArgumentTypeNotes
$urlstringCurrent preview URL (may be empty). Value to return.
$post_idintBeat post ID
$preview?BeatStore\Domain\Previewnull when the beat has no preview meta

Return a non-empty string to use that URL. An empty string or a non-string leaves the existing preview unchanged.

Preview public properties: source (upload, dropbox, soundcloud, or a source you registered), url, attachment_id (?int, upload only).

Core: Dropbox rewrites www.dropbox.comdl.dropboxusercontent.com at priority 10. SoundCloud resolves a permalink to a stream URL at priority 20. When Protect uploaded previews is on, Preview Protection replaces Media Library URLs with signed /beatstore-preview/ links at priority 30. See Protect uploaded previews, Dropbox, and SoundCloud.

add_filter( 'beatstore_resolve_preview_url', function ( $url, $post_id, $preview ) {
	if ( $preview instanceof \BeatStore\Domain\Preview && 'myservice' === $preview->source ) {
		return 'https://cdn.example.com/previews/' . (int) $post_id . '.mp3';
	}
	return $url;
}, 15, 3 );

beatstore_ai_crawler_patterns

User-agent substrings matched case-insensitively when preview protection blocks known AI crawlers on /beatstore-preview/ and POST /beatstore/v1/play.

apply_filters( 'beatstore_ai_crawler_patterns', string[] $patterns )
ArgumentTypeNotes
$patternsstring[]Substrings such as GPTBot, ClaudeBot. Value to return.

Return string[]. Core ships a default list of training / scraping bots.

add_filter( 'beatstore_ai_crawler_patterns', function ( $patterns ) {
	$patterns[] = 'MyScraperBot';
	return $patterns;
} );

beatstore_preview_sources

Register IDs for the Preview source dropdown on the beat metabox. Pair each ID with beatstore_resolve_preview_url so the player can stream it.

apply_filters( 'beatstore_preview_sources', array<string, string> $sources )
ArgumentTypeNotes
$sourcesarray<string, string>Map of source id → label. Value to return.

Return array<string, string>. Empty or invalid maps fall back to upload → “Upload”. Core already adds upload, dropbox, and soundcloud.

add_filter( 'beatstore_preview_sources', function ( $sources ) {
	$sources['myservice'] = 'My CDN';
	return $sources;
} );

beatstore_preview_saved

Fires after BeatStore writes _beatstore_preview (and syncs duration from the file). SoundCloud uses this to clear its resolve cache and refresh duration.

do_action( 'beatstore_preview_saved', int $post_id )
ArgumentTypeNotes
$post_idintBeat that was just saved
add_action( 'beatstore_preview_saved', function ( $post_id ) {
	delete_transient( 'my_preview_' . (int) $post_id );
} );

Echo markup after <beatstore-player> inside .beatstore-player-wrap. Runs for the shortcode, block, Place Player page, and off-site embed — anywhere the player template renders.

do_action( 'beatstore_player_footer' )

No arguments. The player UI lives in an open Shadow DOM; siblings you print here cannot restyle player internals. Use this for a legal line, a custom cart chip, or a noscript fallback.

add_action( 'beatstore_player_footer', function () {
	echo '<p class="my-player-note">Previews are tagged. Licenses checkout on this site.</p>';
} );

beatstore_checkout_before_submit

Echo extra fields on the EDD or Woo checkout form, immediately before the submit button. Pair with beatstore_checkout_required_fields if a field must be filled.

do_action( 'beatstore_checkout_before_submit', string $adapter )
ArgumentTypeNotes
$adapterstring'edd' or 'woocommerce'
add_action( 'beatstore_checkout_before_submit', function ( $adapter ) {
	unset( $adapter );
	echo '<p class="form-row"><label for="artist_name">Artist name</label><input type="text" id="artist_name" name="artist_name" /></p>';
} );

beatstore_checkout_required_fields

Declare POST keys that must be non-empty at checkout. Empty values become an EDD error (edd_set_error) or a Woo notice (wc_add_notice). This filter does not print the inputs — use beatstore_checkout_before_submit for that.

apply_filters( 'beatstore_checkout_required_fields', array $fields, string $adapter )
ArgumentTypeNotes
$fieldsarrayValue to return. See shapes below.
$adapterstring'edd' or 'woocommerce'

Return an array:

ShapeMeaning
'field_name' => 'Please enter your artist name.'Key is the POST name; value is the error message
[ 'field_name' ] (int keys)POST name only; message is “This field is required.”
add_filter( 'beatstore_checkout_required_fields', function ( $fields, $adapter ) {
	unset( $adapter );
	$fields['artist_name'] = 'Artist name is required.';
	return $fields;
}, 10, 2 );

beatstore_offer_cart_actions

Echo extra markup next to a cart line (license name / actions). EDD: last cell of the checkout table row. Woo classic cart: appended to the cart item name. Woo Cart, Checkout, and Mini-cart blocks: Store API item_data (the classic name filter does not run there).

do_action( 'beatstore_offer_cart_actions', int $beat_id, string $offer_id, string $adapter )
ArgumentTypeNotes
$beat_idintDownload / product ID (0 if the line could not be mapped)
$offer_idstringEDD price ID or Woo variation ID (may be '')
$adapterstring'edd' or 'woocommerce'
add_action( 'beatstore_offer_cart_actions', function ( $beat_id, $offer_id, $adapter ) {
	unset( $adapter );
	if ( $beat_id < 1 ) {
		return;
	}
	printf(
		'<p class="description">License %s · beat %d</p>',
		esc_html( $offer_id ),
		(int) $beat_id
	);
}, 10, 3 );

beatstore_contract_customer

Adjust buyer identity before beatstore_order_line_fulfilled. Default comes from the EDD payment or Woo order (email, name, address line 1, user ID).

apply_filters( 'beatstore_contract_customer', CustomerContext $customer, LineItem $line )
ArgumentTypeNotes
$customerBeatStore\Domain\CustomerContextValue to return
$lineBeatStore\Domain\LineItemThe line about to be fulfilled

Return a CustomerContext. Any other type is replaced with an empty context (email and name blank).

CustomerContext public properties: email, name, address, user_id.

add_filter( 'beatstore_contract_customer', function ( $customer, $line ) {
	unset( $line );
	$legal = get_user_meta( $customer->user_id, 'legal_name', true );
	if ( ! is_string( $legal ) || $legal === '' ) {
		return $customer;
	}
	return new \BeatStore\Domain\CustomerContext(
		$customer->email,
		$legal,
		$customer->address,
		$customer->user_id
	);
}, 10, 2 );

beatstore_contract_producer

Adjust producer identity before beatstore_order_line_fulfilled. Default: beat producer_alias or producer_full_name meta, else BeatStore → Settings producer name, else the site title.

apply_filters( 'beatstore_contract_producer', ProducerContext $producer, LineItem $line )
ArgumentTypeNotes
$producerBeatStore\Domain\ProducerContextValue to return
$lineBeatStore\Domain\LineItemThe line about to be fulfilled

Return a ProducerContext. Any other type falls back to the default derived from the beat.

ProducerContext public properties: name, legal_name, beat_id.

add_filter( 'beatstore_contract_producer', function ( $producer, $line ) {
	$legal = (string) get_post_meta( $line->beat_id, 'producer_full_name', true );
	if ( $legal === '' ) {
		return $producer;
	}
	return new \BeatStore\Domain\ProducerContext( $producer->name, $legal, $line->beat_id );
}, 10, 2 );

beatstore_order_line_fulfilled

Fires once per cart line after EDD edd_complete_purchase or Woo processing / completed. Woo stores _beatstore_fulfilled on the order so both statuses do not double-fire. This runs for every line, not only exclusives. Core does not write a PDF here — a contracts add-on should.

Customer and producer are already passed through the two filters above.

do_action( 'beatstore_order_line_fulfilled', LineItem $line, CustomerContext $customer, ProducerContext $producer )
ArgumentTypeNotes
$lineBeatStore\Domain\LineItemFulfilled line
$customerBeatStore\Domain\CustomerContextAfter beatstore_contract_customer
$producerBeatStore\Domain\ProducerContextAfter beatstore_contract_producer

LineItem public properties: beat_id, offer_id, commerce_ref ({adapter}:{beat_id}:{offer_id}), order_ref (EDD payment ID or Woo order ID as a string), adapter (edd / woocommerce), title, amount.

add_action( 'beatstore_order_line_fulfilled', function ( $line, $customer, $producer ) {
	error_log( sprintf(
		'Fulfilled %s for %s (%s) from %s',
		$line->commerce_ref,
		$customer->email,
		$line->order_ref,
		$producer->name
	) );
}, 10, 3 );

beatstore_sold_state_changed

Fires after an exclusive purchase writes _beatstore_sold (at, order_ref, offer_id). The beat stays published. There is no matching “unsold” action in core — clearing sold is a manual meta edit.

do_action( 'beatstore_sold_state_changed', int $post_id, string $order_ref )
ArgumentTypeNotes
$post_idintBeat that is now sold
$order_refstringEDD payment ID or Woo order ID
add_action( 'beatstore_sold_state_changed', function ( $post_id, $order_ref ) {
	wp_mail(
		get_option( 'admin_email' ),
		'Exclusive sold',
		sprintf( 'Beat %d sold on order %s', $post_id, $order_ref )
	);
}, 10, 2 );

beatstore_register_modules

Fires at the end of plugin boot, after Upload / Dropbox / SoundCloud / Mailchimp / Preview Protection are registered. Call $plugin->register_module() with an object that implements BeatStore\Modules\ModuleInterface (id() and register()).

do_action( 'beatstore_register_modules', \BeatStore\Plugin $plugin )
ArgumentTypeNotes
$plugin\BeatStore\PluginUse register_module(), beats(), commerce(), options()
add_action( 'beatstore_register_modules', function ( $plugin ) {
	$plugin->register_module( new \MyPlugin\BeatStore\CdnPreview() );
} );

beatstore_commerce_adapters

Register another commerce backend. Fires after none / EDD / Woo are registered and before the registry picks the active adapter (Settings → Commerce, or the first available).

do_action( 'beatstore_commerce_adapters', \BeatStore\Commerce\Registry $registry )
ArgumentTypeNotes
$registry\BeatStore\Commerce\RegistryCall register( AdapterInterface $adapter )

AdapterInterface methods: id(), is_available(), product_post_type(), get_offers(), save_offers(), add_to_cart(), cart_state(), checkout_url(). Exactly one adapter is active per site.

add_action( 'beatstore_commerce_adapters', function ( $registry ) {
	$registry->register( new \MyPlugin\BeatStore\CustomAdapter() );
} );

beatstore_integrations_cards

Echo admin cards inside BeatStore → Integrations. Core uses this for Mailchimp, SoundCloud, and Dropbox. Markup is a .postbox in a CSS grid; keep credentials in WordPress options, not wp-config.php. See Integrations.

do_action( 'beatstore_integrations_cards' )

No arguments.

add_action( 'beatstore_integrations_cards', function () {
	echo '<div class="postbox"><div class="postbox-header"><h2 class="hndle">My CDN</h2></div>';
	echo '<div class="inside"><p>Connected. Tokens live in the WordPress options table.</p></div></div>';
} );

beatstore_settings_after

Fires on BeatStore → Settings after the #beatstore-settings React root and the empty #beatstore-settings-contracts mount, before the player preview. beatstore-contracts mounts its wp.components panel there. Options still live in beatstore_contracts_settings, not beatstore_settings.

do_action( 'beatstore_settings_after' )

No arguments.