Hook: mam_iap_purchase

Internal dispatcher hook. This filter is how mam-main’s AJAX pipeline asks mam-inapp-purchase-manager to record a receipt. It is documented here because the surface matters for debugging and for plugins that need to observe purchases — not because typical extensions should subscribe to it. If you want to react to a purchase, subscribe to it as an action-style observer; if you want to override subscription state, use Hook: mam_iap_active_subscriber_product instead.


Signature

apply_filters( 'mam_iap_purchase', $unused );

The filter does not pass a meaningful first argument and does not return a meaningful value. It is treated by the plugin as a dispatcher — the act of applying the filter is the trigger; all input arrives via $_REQUEST.

The plugin’s callback never returns; it ends with wp_send_json( ['status' => 'success'] ) and exits the request.


Inputs (read from $_REQUEST)

Key Required Used as
$_REQUEST['product'] yes Product SKU. Looked up via wc_get_product_id_by_sku(). Also written to iap_product order meta.
$_REQUEST['expdatestamp'] yes Expiration timestamp string from the platform receipt. = padding is stripped. Used in deduplication and stored as iap_expiration_date order meta.
$_REQUEST['pid'] no Branch.io campaign attribution lookup key. Optional.

The plugin also resolves the buying user via do_action( 'mam_update_current_user' ) and reads $mam_user_id globally.


What it does

In mam_in_app_purchase_manager::add_purchase() (mam-inapp-purchase-manager.php):

  1. Logs the incoming product and expdatestamp to mamdebug.
  2. Resolves the current MAM user. Bails if $mam_user_id <= 0.
  3. Strips trailing = padding from expdatestamp.
  4. Looks up the WooCommerce product by SKU. Bails silently if not found.
  5. Deduplicates. Queries for existing wc-completed shop_order posts with matching iap_expiration_date + iap_product meta. If one exists, skips order creation.
  6. Computes the expiration date from the product’s duration attribute term (1 Day+1 day, 1 Month+1 month, 1 Quarter+3 months, 1 Year+1 year). Anything else leaves the expiration unchanged from time().
  7. Resolves attribution. If branch_app_id option is set and the mam_deep_link_data table exists, looks up the venue_code for pid and uses its post title as the UTM source. Otherwise defaults to Mobile App.
  8. Creates a new wc-completed WooCommerce order, attaches the product, sets attribution metadata, and saves.
  9. Forces post_status = 'wc-completed' via wp_update_post() (belt-and-braces against status filters).
  10. Writes iap_expiration_date, iap_product, and (if the computed expiration is in the future) expiration_date post meta.
  11. Sends {"status":"success"} and exits.

When it runs

mam-main’s AJAX pipeline is the canonical caller. The mobile app reports a successful purchase to a mam-main endpoint, which validates the session and applies mam_iap_purchase. The plugin is registered at default priority (10) and is the only first-party callback.

The hook is not fired anywhere else in the codebase.


Example: observe purchases without changing behavior

add_filter( 'mam_iap_purchase', 'my_app_log_iap_purchase', 5 );

function my_app_log_iap_purchase( $value ) {
    global $mam_user_id;
    error_log( sprintf(
        '[IAP] user=%d product=%s exp=%s',
        $mam_user_id,
        sanitize_text_field( $_REQUEST['product'] ?? '' ),
        sanitize_text_field( $_REQUEST['expdatestamp'] ?? '' )
    ) );
    return $value;
}

Register at priority 5 to run before the plugin’s order-creation callback at priority 10. Always return $value — even though the plugin doesn’t use the return for anything, the WordPress filter machinery does, and a missing return strips the chain.

The plugin’s callback ends the request with wp_send_json(), so anything you register at priority > 10 will not execute.


Example: skip recording for a specific test SKU

This is not a recommended pattern — it short-circuits the order pipeline with no admin signal that anything was suppressed — but it illustrates how priority works:

add_filter( 'mam_iap_purchase', 'my_app_skip_test_skus', 5 );

function my_app_skip_test_skus( $value ) {
    $sku = sanitize_text_field( $_REQUEST['product'] ?? '' );
    if ( str_starts_with( $sku, 'test_' ) ) {
        wp_send_json( [ 'status' => 'skipped' ] );
    }
    return $value;
}

Because the plugin’s callback also calls wp_send_json() and exits, the only way to “cancel” the order without modifying core is to short-circuit and exit yourself before priority 10 runs.


Gotchas

  • $_REQUEST is the input, not the filter argument. The filter signature only exists to dispatch; everything semantically interesting comes from $_REQUEST['product'], $_REQUEST['expdatestamp'], and the resolved $mam_user_id. A null first arg is normal.
  • No server-side receipt validation. The plugin trusts the values in $_REQUEST. Apple and Google remain the source of truth for whether a purchase actually happened. Don’t rely on the WooCommerce order existing as proof of payment.
  • Duplicate-receipt detection is by expdatestamp + product only. Apple sometimes re-sends receipts. The plugin will not double-record those. But two distinct purchases with the same expiration timestamp (rare but possible at minute granularity) will collide.
  • expdatestamp is partially sanitized. Only trailing = is stripped. The raw value is stored to order meta. If you’re reading iap_expiration_date meta downstream, sanitize on read.
  • The full $_REQUEST is logged twice. mamdebug writes to the admin-only debug log on entry and exit. If $_REQUEST contains receipt blobs or shared secrets, they end up in the log. The log table is admin-only but treat it as sensitive.
  • The callback exits the request. Anything you register at priority > 10 is never invoked, and any code after apply_filters( 'mam_iap_purchase', ... ) in the caller is unreachable for the success path.
  • Unknown duration strings silently fail to set an expiration. Only the exact strings 1 Day, 1 Month, 1 Quarter, 1 Year map to expirations. Anything else (including One Time, lowercase variants, custom terms) creates the order but never writes the expiration_date meta. Hook: mam_iap_active_subscriber_product keys on expiration_date, so such users won’t be recognized as subscribers.

Verification

This article was last verified against:

  • Plugin: mam-inapp-purchase-manager v2.0
  • Source: mam-inapp-purchase-manager.php (add_purchase)

Re-verify whenever the duration → expiration mapping changes, the deduplication query keys change, the iap_product / iap_expiration_date / expiration_date meta keys are renamed, the attribution branch (branch_app_id + mam_deep_link_data) changes, or the callback stops calling wp_send_json().


  • Plugin overview: mam-inapp-purchase-manager
  • Recipe: Create an IAP product
  • Hook: mam_iap_active_subscriber_product
  • Hook: mam_iap_require_iap

Metadata

Field Value
Article type Hook Reference
Plugin slug mam-inapp-purchase-manager
Applies to plugin version 2.0+
Category Extending MAM Suite
Hook type filter
Audience PHP developer
Last verified 2026-05-01
Contents

    Need Support?

    Can’t find the answer you’re looking for? Don’t worry we’re here to help!