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.
| Hook | Type | Arity | Use |
|---|---|---|---|
beatstore_beat_offers | filter | 2 | License offers before REST / the player |
beatstore_resolve_preview_url | filter | 3 | Resolve or override a preview URL |
beatstore_ai_crawler_patterns | filter | 1 | User-agent substrings blocked on protected preview paths |
beatstore_preview_sources | filter | 1 | Register preview source modules |
beatstore_preview_saved | action | 1 | After a beat preview is saved |
beatstore_player_footer | action | 0 | Markup after the player |
beatstore_checkout_before_submit | action | 1 | Echo fields before checkout submit |
beatstore_checkout_required_fields | filter | 2 | Required checkout field names |
beatstore_offer_cart_actions | action | 3 | Extra markup per cart line |
beatstore_contract_customer | filter | 2 | Buyer identity for a fulfilled line |
beatstore_contract_producer | filter | 2 | Producer identity for a fulfilled line |
beatstore_order_line_fulfilled | action | 3 | After a line is delivered |
beatstore_sold_state_changed | action | 2 | Exclusive just marked sold |
beatstore_register_modules | action | 1 | Register feature modules |
beatstore_commerce_adapters | action | 1 | Register a commerce backend |
beatstore_integrations_cards | action | 0 | Cards on BeatStore → Integrations |
beatstore_settings_after | action | 0 | After 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 )
| Argument | Type | Notes |
|---|---|---|
$offers | BeatStore\Domain\Offer[] | Value to return. Each item must stay an Offer instance. |
$post_id | int | EDD 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 )
| Argument | Type | Notes |
|---|---|---|
$url | string | Current preview URL (may be empty). Value to return. |
$post_id | int | Beat post ID |
$preview | ?BeatStore\Domain\Preview | null 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.com → dl.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 )
| Argument | Type | Notes |
|---|---|---|
$patterns | string[] | 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 )
| Argument | Type | Notes |
|---|---|---|
$sources | array<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 )
| Argument | Type | Notes |
|---|---|---|
$post_id | int | Beat that was just saved |
add_action( 'beatstore_preview_saved', function ( $post_id ) {
delete_transient( 'my_preview_' . (int) $post_id );
} );
beatstore_player_footer
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 )
| Argument | Type | Notes |
|---|---|---|
$adapter | string | '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 )
| Argument | Type | Notes |
|---|---|---|
$fields | array | Value to return. See shapes below. |
$adapter | string | 'edd' or 'woocommerce' |
Return an array:
| Shape | Meaning |
|---|---|
'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 )
| Argument | Type | Notes |
|---|---|---|
$beat_id | int | Download / product ID (0 if the line could not be mapped) |
$offer_id | string | EDD price ID or Woo variation ID (may be '') |
$adapter | string | '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 )
| Argument | Type | Notes |
|---|---|---|
$customer | BeatStore\Domain\CustomerContext | Value to return |
$line | BeatStore\Domain\LineItem | The 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 )
| Argument | Type | Notes |
|---|---|---|
$producer | BeatStore\Domain\ProducerContext | Value to return |
$line | BeatStore\Domain\LineItem | The 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 )
| Argument | Type | Notes |
|---|---|---|
$line | BeatStore\Domain\LineItem | Fulfilled line |
$customer | BeatStore\Domain\CustomerContext | After beatstore_contract_customer |
$producer | BeatStore\Domain\ProducerContext | After 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 )
| Argument | Type | Notes |
|---|---|---|
$post_id | int | Beat that is now sold |
$order_ref | string | EDD 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 )
| Argument | Type | Notes |
|---|---|---|
$plugin | \BeatStore\Plugin | Use 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 )
| Argument | Type | Notes |
|---|---|---|
$registry | \BeatStore\Commerce\Registry | Call 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.