Summary
The JSON payload returned by the local_app_get_phone_data AJAX action is the contract between the WordPress site and the mobile app. Mobile clients in the field consume specific keys at specific paths; the shape is frozen at the top level.
This article catalogs the top-level structure, the per-section conventions, and what sibling plugins must obey when injecting.
Top-level shape
{
"user": { "id": "0", "role": "anonymous", ... },
"settings": { "theme": {...}, "branding": {...}, ... },
"main_button_array": [ { ... }, { ... }, ... ],
"left_button_array": [ { ... }, { ... }, ... ],
"tab_bar_settings": { "buttons": [...], "visible": true },
"form_data": { "<form_id>": { ... }, ... },
"nonce": "<wp-nonce>",
"ajax_url": "https://example.com/wp-admin/admin-ajax.php",
"home_cats": { ... },
"notifications": [ { ... }, ... ],
"unread_count": 3,
"favorites": [ "123", "456" ],
"report_favorites": "yes",
"geofilter_default_name": "City",
"track_views": "yes",
"<sibling_plugin_keys>": ...
}
Keys not listed above are sibling-plugin contributions injected at mam_get_phone_data_before_send.
Conventions
yes / no strings, not booleans
Many flags ship as strings ("yes" / "no" or "on" / "off") rather than booleans. Frozen — older mobile parsers expect strings. Don’t ship true/false in places where the convention is "yes".
Strings for numeric ids
Post ids, user ids, button ids, and form indexes are typed as strings in the payload, not integers. Older iOS / Android JSON parsers reject numeric ids in places where strings were established.
null becomes ""
mam_replace_null_with_empty_string() runs in phase_finalize and replaces every null in field values with "". Older parsers raised on null in string fields. Don’t try to undo this transformation.
Section root null means “no change”
The cursor cache mechanism uses null at the section root to signal “no change since cursor”. That’s a different semantic from null inside fields. mam_replace_null_with_empty_string() only touches field values, not section roots.
Frozen top-level keys
The following keys are observed and parsed by deployed mobile clients. Renaming them breaks apps in the field:
| Key | Role | Frozen by |
|---|---|---|
user.id, user.role |
Auth state | mam-main |
main_button_array, left_button_array |
Nav button arrays | mam-main |
tab_bar_settings |
Tab bar config | mam-main |
form_data |
Form definitions keyed by form id | forms-manager |
nonce |
WordPress nonce for subsequent AJAX | mam-main |
ajax_url |
The admin-ajax.php URL | mam-main |
home_cats |
Home-screen category structure | mam-main (MAM_Main_Manager::manage_phone_data) |
notifications, unread_count |
In-app notification list | notifications-manager |
favorites, report_favorites, hide_remove_all_favorites |
Favorites sync | mam-main + sibling plugins |
geofilter_default_name |
Default city | mam-geofilters |
track_views |
Analytics opt-in flag | various |
Sibling-plugin injection
Sibling plugins inject their own top-level keys via mam_get_phone_data_before_send. The convention:
- Use a plugin-specific prefix (
my_plugin_listings, notlistings) - Don’t overwrite existing keys
- Don’t introduce keys that conflict with frozen names
- Use string types for ids and
"yes"/"no"for flags
Examples from existing plugins (mam-geodirectory):
| Key | Shape | Plugin |
|---|---|---|
track_views |
'yes' |
mam-geodirectory |
report_favorites |
'yes' |
mam-geodirectory |
hide_remove_all_favorites |
'yes' |
mam-geodirectory |
data_has_search_index |
'yes' |
mam-geodirectory |
use_category_in_search_results |
'yes' |
mam-geodirectory |
do_not_sort_picker |
'yes' |
mam-geodirectory |
geofilter_default_name |
string | mam-geodirectory |
has_initial_form_to_display |
'yes' |
mam-geodirectory |
initial_form_to_display |
int (1-based form index) | mam-geodirectory |
Common section shapes
Button (nav button)
{
"id": "btn_42",
"title": "Home",
"icon": "black_home",
"action": "open_screen",
"source": "screen_id_or_form_index",
"values": { ... },
"visible": "on",
"style_*": { ... }
}
Form definition
{
"id": "42",
"title": "Contact us",
"fields": [
{ "id": "1", "type": "text", "label": "Your name", "required": true },
...
],
"settings": { "colors": {...}, "submit_label": "Send" }
}
Notification
{
"id": "1",
"title": "Your order shipped",
"body": "Tracking: ABC123",
"channel": "push",
"status": "sent",
"ts": "2026-05-02T...",
"data": { ... }
}
Map marker
{
"id": "post_123",
"title": "Venue name",
"lat": "40.7128",
"lon": "-74.0060",
"marker_icon": "https://...",
"snippet": "..."
}
Gotchas
nullis dangerous. Older parsers fail onnullin string fields. The pipeline strips fielded nulls automatically; if you bypass output shaping, you’ll regret it.- Frozen top-level keys. Don’t try to “clean up” key naming — apps in the field reference the existing names.
- Strings for ids, not ints. Even when an id is numeric, ship it as
"123"not123. yes/nostrings, not bools. Same reason.- Don’t overwrite sibling-plugin keys. Two plugins fighting over
listingsproduces last-writer-wins; coordinate via a shared key prefix. - Cursor
nullvs fieldnullare different. Section-rootnullmeans “no change since cursor”; fieldnullbecomes"".
Related articles
- Phone data pipeline overview
- Phone data pipeline phases
- Cursor cache mechanism
- Hook: mam_get_phone_data_before_send
- Frozen public contracts reference
Metadata
| Field | Value |
|---|---|
| Article type | JSON Key Reference |
| Plugin slug | mam-main |
| Applies to plugin version | 2.1.11+ |
| Category | App Settings Reference |
| Audience | PHP developer |
| Last verified | 2026-05-02 |
