Inject custom data into the mobile JSON payload

Goal

Add a top-level key — or augment existing nested data — in the JSON payload the
mobile app downloads on launch, so your sibling plugin’s content reaches the
client. You will do this by subscribing to the phone-data pipeline’s filter,
following the frozen JSON conventions, and integrating with the cursor cache so
you don’t slow down every app launch.

This is the standard, supported extension point for getting custom server-side
data into the app. If you instead need to replace the entire response (an
unusual, full-override case), see the note on mam_local_app_data under
Related.


Prerequisites

  • A sibling plugin with its own slug, loaded after mam-main.
  • mam-main active (it owns the phone-data pipeline and fires the filter).
  • A clear idea of the data you want to ship and at what shape.
  • Coordination with the mobile client team. Server-side injection is only half
    the work — the app must be taught to read and render your key, or the data is
    shipped and ignored.

How the payload is built

When the app launches it calls the local_app_get_phone_data AJAX action.
mam-main runs that request through MAM_Phone_Data_Pipeline, which assembles
the payload in phases and, near the end, fires:

$filtered = apply_filters( 'mam_get_phone_data_before_send', $context->to_array() );

Every plugin that contributes to the mobile payload — app settings, user roles,
notifications, forms, home_cats — does so by subscribing to this one filter at
a chosen priority. Your subscriber receives the accumulated $data_array, adds
or transforms keys, and returns the array. After the filter, the pipeline runs
output shaping (mam_replace_null_with_empty_string()), stamps the cursor, and
sends the JSON.


Steps

1. Choose a top-level key

Prefix the key with your plugin slug so it can’t collide with another
subscriber: my_plugin_widget, my_plugin_listings, my_plugin_alerts. Avoid
bare names like widget or listings — those will eventually collide with
another plugin or with a core key.

2. Pick a priority

The filter runs subscribers in priority order, and order matters because some
keys are read or finalized by later subscribers. As a rough guide:

If your subscriber… Use priority
Only injects its own data, reads nothing 10 (default)
Must run before home_cats is built below 1000
Reads data injected by another plugin 100
Validates already-assembled form state 199
Performs a full use-case override 9999

Most subscribers use the default cohort (10). Priority 1000 is reserved by
mam-main for home_cats and the login format — don’t register there. If you
aren’t sure, start at 10 and adjust only if you find you’re reading a key that
another plugin sets.

3. Register the subscriber

add_filter(
    'mam_get_phone_data_before_send',
    function ( array $data_array ): array {

        $user_id = mam_user_id();

        $data_array['my_plugin_widget'] = array(
            'enabled' => 'yes',
            'message' => 'Hello from my plugin',
            'items'   => $this->get_items_for_user( $user_id ),
        );

        return $data_array;
    },
    10
);

Always return $data_array. Returning null, false, or a fresh array
silently drops every other subscriber’s contribution.

4. Follow the frozen JSON conventions

Mobile parsers already deployed in the field expect specific primitive shapes.
These are part of the frozen mobile contract — match them exactly:

  • Strings for ids, even when the value is numeric: (string) $item_id, not
    (int) $item_id.
  • "yes" / "no" for flags, not true / false.
  • Plain integers are fine for non-id numbers (counts, indexes).
$data_array['my_plugin_widget'] = array(
    'enabled' => 'yes',                  // not true
    'item_id' => (string) $item_id,      // not (int)
    'count'   => count( $items ),        // int is fine here
);

A note on null: during output shaping, mam_replace_null_with_empty_string()
walks the payload recursively and replaces every null — at any depth,
including a whole section value like my_plugin_widget — with a single space
string (" "). No null survives to the wire, so don’t rely on one to signal
anything; when you have no data, emit a concrete empty value (an empty string or
empty array), not null.

5. Integrate with the cursor cache

The cursor is a single global value — the mam_json_cursor option, a
uniqid() string — that the app stores and sends back as ?cursor= on every
request. If the value the app sends matches the server’s current cursor, the
pipeline short-circuits the entire payload with json_status = 'no_new_data'
and the app reuses its cached copy; your subscriber never even runs. If it
doesn’t match, the whole response is rebuilt and every subscriber runs. Caching
is all-or-nothing across the response — there’s no per-section skip — so
“integrating” means keeping the cursor honest: it must change when your data
changes and stay put when it doesn’t.

mam-main already bumps the cursor on the common WordPress writes — post
save/delete, post-, term-, and user-meta changes, and term and user changes — via
JSON_Cursor_Manager::setup_hooks(). If your data lives in posts, meta, terms,
or users, it’s already invalidated whenever it changes. If your data changes
through a path those hooks don’t cover — an external sync, a custom option, a
remote fetch — reset the cursor yourself right after you write it, so the next
request rebuilds:

// After mutating your plugin's data through a non-WP-core path:
JSON_Cursor_Manager::reset_cursor();

Don’t call reset_cursor() on unrelated writes: it invalidates every app’s
cache and forces a full rebuild for every user on their next launch. And because
that rebuild reruns your subscriber for every user, keep the filter cheap (see
Anti-patterns).

6. Handle anonymous users and cloning admins

Read the current request through the mam_user(), mam_user_id(), and
mam_is_cloning() helpers rather than touching globals directly. Skip work when
there’s no user, and when an admin is cloning (previewing the app as another
role) prefer fresh data over a cached section:

if ( ! mam_user() ) {
    return $data_array; // anonymous — nothing to inject
}

if ( mam_is_cloning() ) {
    // Previewing as another role — rebuild rather than trust a stale section.
}

7. Verify in the payload

Snapshot the JSON before and after activating your plugin and diff the result:

curl 'https://example.com/wp-admin/admin-ajax.php?action=local_app_get_phone_data' > before.json
# activate your plugin, then:
curl 'https://example.com/wp-admin/admin-ajax.php?action=local_app_get_phone_data' > after.json
diff before.json after.json

Confirm your key appears at the right level with the right primitive shapes
(string ids, yes/no flags). mam-main also ships a snapshot regression
harness under tests/snapshot/ that can capture the payload systematically.

8. Hand off to the mobile team

Tell the client team the key name, its shape, and which screen should render it.
Until the app is updated to read your key, the data ships but does nothing.


Anti-patterns to avoid

  • Making uncached HTTP calls inside the filter — every millisecond is paid on
    every app launch, across every subscriber.
  • Running one DB query per row instead of batching — that cost multiplies by
    subscribers and by users.
  • Overwriting a key you don’t own. Stay inside your prefixed namespace.
  • Returning null, false, or a fresh array from the filter — always return
    the $data_array you were given, or you drop every other subscriber’s keys.
  • Registering at priority 1000 (reserved) or calling reset_cursor() on
    unrelated writes, which invalidates every app’s cache.

  • Recipe: Inject custom data into the mobile JSON (mam-main, source recipe for this guide)
  • Hook: mam_get_phone_data_before_send — the filter reference
  • Hook: mam_local_app_data — the full-replacement short-circuit (use only when you must replace, not augment, the response)
  • Phone data pipeline overview and Mobile JSON shape — the conventions in full
  • Cursor cache mechanism — how the per-install timestamp gates rebuilds
Was this article helpful?
Contents

    Need Support?

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