Internal dispatcher hook. This filter is how mam-main’s AJAX pipeline asks
mam-inapp-purchase-managerto 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):
- Logs the incoming
productandexpdatestamptomamdebug. - Resolves the current MAM user. Bails if
$mam_user_id <= 0. - Strips trailing
=padding fromexpdatestamp. - Looks up the WooCommerce product by SKU. Bails silently if not found.
- Deduplicates. Queries for existing
wc-completedshop_orderposts with matchingiap_expiration_date+iap_productmeta. If one exists, skips order creation. - Computes the expiration date from the product’s
durationattribute term (1 Day→+1 day,1 Month→+1 month,1 Quarter→+3 months,1 Year→+1 year). Anything else leaves the expiration unchanged fromtime(). - Resolves attribution. If
branch_app_idoption is set and themam_deep_link_datatable exists, looks up thevenue_codeforpidand uses its post title as the UTM source. Otherwise defaults toMobile App. - Creates a new
wc-completedWooCommerce order, attaches the product, sets attribution metadata, and saves. - Forces
post_status = 'wc-completed'viawp_update_post()(belt-and-braces against status filters). - Writes
iap_expiration_date,iap_product, and (if the computed expiration is in the future)expiration_datepost meta. - 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
$_REQUESTis 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. Anullfirst 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+productonly. 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. expdatestampis partially sanitized. Only trailing=is stripped. The raw value is stored to order meta. If you’re readingiap_expiration_datemeta downstream, sanitize on read.- The full
$_REQUESTis logged twice.mamdebugwrites to the admin-only debug log on entry and exit. If$_REQUESTcontains 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
> 10is never invoked, and any code afterapply_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 Yearmap to expirations. Anything else (includingOne Time, lowercase variants, custom terms) creates the order but never writes theexpiration_datemeta. Hook: mam_iap_active_subscriber_product keys onexpiration_date, so such users won’t be recognized as subscribers.
Verification
This article was last verified against:
- Plugin:
mam-inapp-purchase-managerv2.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().
Related articles
- 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 |
