Goal
Add a top-level key (or augment existing nested data) to the JSON payload returned by local_app_get_phone_data, so the mobile app can render your sibling plugin’s content.
Prerequisites
- A sibling plugin with its own slug
- A clear understanding of what data you want to inject
- Coordination with the mobile client team (the app must know how to render your key)
Steps
1. Pick the right priority
| If your subscriber… | Use priority |
|---|---|
| Reads no other data, only injects | 10 (default cohort) |
Adds to home_cats |
10 (must run before 1000) |
| Reads listings injected by another plugin | 100 |
| Validates form state | 199 |
| Performs a use-case total override | 9999 |
Most subscribers use 10. See Priority conventions for phone-data subscribers.
2. Choose a top-level key
Convention: prefix with your plugin slug to avoid collisions. my_plugin_widget, my_plugin_listings, my_plugin_alerts. Don’t use bare names like widget or listings — those collide.
3. Register the subscriber
add_filter( 'mam_get_phone_data_before_send',
function ( array $data_array ): array {
$user_id = mam_current_request()->user_id();
$data_array['my_plugin_widget'] = array(
'enabled' => true,
'message' => 'Hello from my plugin',
'items' => $this->get_items_for_user( $user_id ),
);
return $data_array;
},
10
);
4. Use string-typed ids and yes/no flags
Mobile parsers in the field expect strings for ids and "yes"/"no" for flags:
$data_array['my_plugin_widget'] = array(
'enabled' => 'yes', // not true
'item_id' => (string) $item_id, // not (int)
'count' => count( $items ), // ints fine for non-id numbers
);
5. Wire up cursor caching
If your data is expensive to build, integrate with the cursor mechanism so the app can skip rebuilding when nothing has changed:
add_filter( 'mam_get_phone_data_before_send',
function ( array $data_array ): array {
$cursor = JSON_Cursor_Manager::get_cursor( get_current_user_id() );
$section_ts = (int) get_option( 'my_plugin_data_changed_at', 0 );
if ( $section_ts <= $cursor ) {
$data_array['my_plugin_widget'] = null; // signal "no change"
return $data_array;
}
$data_array['my_plugin_widget'] = $this->build_widget();
return $data_array;
},
10
);
When you mutate underlying data, bump the timestamp:
update_option( 'my_plugin_data_changed_at', time() );
For changes that affect every user (a setting change), call JSON_Cursor_Manager::reset_cursor().
6. Test in the Previewer
# Snapshot the JSON before adding your plugin:
curl 'https://example.com/wp-admin/admin-ajax.php?action=local_app_get_phone_data' > before.json
# Activate your plugin, then snapshot again:
curl 'https://example.com/wp-admin/admin-ajax.php?action=local_app_get_phone_data' > after.json
# Diff:
diff before.json after.json
Verify your key appears at the right level with the right shape. The tests/snapshot/snapshot-phone-data.php regression harness can be used for systematic verification.
7. Coordinate with the mobile team
Server-side injection is half the work. The mobile client must know how to consume your key. Without client support, the data is shipped but ignored.
Patterns to follow
Per-user data
$user_id = mam_current_request()->user_id();
$data_array['my_plugin_user_data'] = $user_id ? $this->build_user_data($user_id) : null;
Authentication-aware
if ( ! mam_current_request()->user() ) {
return $data_array; // anonymous — skip
}
Cloning admins
Cloning admins are previewing as another role. bypass_caching is on; your section will rebuild every request.
if ( mam_current_request()->is_cloning() ) {
// Always rebuild — don't trust the cache
}
Anti-patterns to avoid
- ❌ Make HTTP calls without caching — a 500ms call adds 500ms per app launch
- ❌ Run unbatched DB queries (one query per row × 70 subscribers × every user)
- ❌ Overwrite keys you don’t own — coordinate via prefixed keys
- ❌ Return
nullfrom the filter (return$data_array) - ❌ Use bool / int types where the convention is string
- ❌ Defeat the cursor by always rebuilding
- ❌ Register at priority 1000 (reserved for
home_cats)
Frozen JSON conventions
- Top-level key naming — prefix with your plugin slug
- Strings for ids — even when numeric (
"123"not123) "yes"/"no"for flags — nottrue/falsenullbecomes""in field values viamam_replace_null_with_empty_string()— section-rootnullis preserved (cursor signal)
See Mobile JSON shape for the full conventions.
Related articles
- Phone data pipeline overview
- Phone data pipeline phases
- Mobile JSON shape
- Cursor cache mechanism
- Priority conventions for phone-data subscribers
- Hook: mam_get_phone_data_before_send
- Hook: mam_local_app_data
- Frozen public contracts reference
Metadata
| Field | Value |
|---|---|
| Article type | Recipe (Developer) |
| Plugin slug | mam-main |
| Applies to plugin version | 2.1.11+ |
| Category | Extending MAM Suite |
| Audience | PHP developer |
| Estimated time | 30–60 minutes |
| Last verified | 2026-05-02 |
