Querying Moast data
Updated 2 weeks ago
This guide is for developers who need Moast's data from outside the Moast admin: a merchant's theme, a partner app, a page builder, an SEO tool, or a script. There are several ways in, and picking the right one saves most of the work.
You are… | Use |
|---|---|
A third-party app holding a Storefront API token | Shopify's own APIs. Moast's metaobjects and metafields are |
Writing Liquid in a merchant's theme | Same data, same namespaces, read through Liquid. |
Running JavaScript on the storefront, or on any other page | The app proxy HTTP API at |
Building a crawler or SEO integration |
|
Holding only your own Admin API session | You cannot read Moast's |
Everything Moast publishes to a shop is derived state. Moast rewrites it on every save and on background re-bakes, so read it, never write it.
The two namespaces
Moast writes into two Shopify namespaces, and the difference decides who can read what.
$app is Shopify's app-reserved namespace, rendered app--108560121857. Only Moast writes it, and which API you use decides whether you can read it:
Storefront API and Liquid: yes. Moast declares
storefront: PUBLIC_READon the widget metaobjects and the per-widget product and collection metafields, so any caller with a storefront access token reads them like any other public metafield. This is the path for a third-party app.Admin API: no. Shopify closed every route for granting another app read access here.
merchant_readbuys the merchant a look in their own admin, not another app. Do not plan on reading Moast's$appdata with your own Admin API session.
moast is a plain merchant-owned namespace, readable over both APIs. Anything meant to be read by something other than Moast lives here. The trade is that the data becomes the merchant's: it survives an uninstall, and a merchant could in principle edit it.
The only $app value with no storefront access at all is $app:feed, covered below. Everything else in the tables is storefront-readable.
Moast's app id
Every $app read needs Moast's numeric app id, which is 108560121857. So the full namespace is app--108560121857, and the widget metaobject type is app--108560121857--moast_widget.
This value is the same on every shop: it identifies the app, not the installation, and Moast bakes it into its theme extension at build time. Hardcode it; there is nothing to look up per merchant.
To sanity-check it, query a product you know carries Moast media over the Storefront API. A non-null result means the namespace resolved:
query MoastNamespaceCheck($handle: String!) {
product(handle: $handle) {
metafields(
identifiers: [{ namespace: "app--108560121857", key: "<widget-ref>" }]
) {
key
value
}
}
}Only a dev or staging Moast build uses a different id, because those are separate Shopify apps. If you are pointed at a development store and the namespace does not resolve, ask Moast for that build's id rather than guessing.
Metaobjects
Moast publishes two metaobject types. Both are MERCHANT_READ in the admin and PUBLIC_READ on the storefront.
The widget metaobject
The rendered type is app--108560121857--moast_widget, with one entry per widget. The handle is the widget's ref.
Field | Type | Holds |
|---|---|---|
|
| Merchant-facing widget name |
|
|
|
|
| Fully baked storefront HTML (see below) |
|
|
|
|
| The handle again, for callers that only have the entry |
content is HTML, not JSON. It is a <moast-*> custom element whose attributes carry the widget's settings, with a <script type="application/json" class="data"> child carrying media and products.
To get structured data out of it, either parse that script tag or, much better, call the widget endpoint in the app proxy API section, which does exactly that parsing for you.
Two limits bite here. First, Shopify caps a multi_line_text_field at 65,536 characters, so Moast drops media from the end of a media-heavy widget to make the write land. Second, for a per-product or per-collection widget, content is deliberately baked empty because the real markup lives on the product's or collection's own metafield. An empty content on such a widget is correct, not a bug.
The legacy feed metaobject
The rendered type is app--108560121857--moast_feed, with the same access. The handle is the feed's ref, or default for the shop's default feed.
Field | Type | Holds |
|---|---|---|
|
| Feed name |
|
| The feed payload (media, sort, conditions) |
|
| Products tagged across the feed's media, capped at 128 |
Feeds predate widgets. A widgets-era install grows no new feed rows, and the definition is created by a background task rather than declared in the app config, so a shop may not have it at all. Prefer widgets for anything new.
The data payload:
{
"name": "Default",
"conditions": [],
"disjunctive": false,
"sort": "MANUAL",
"maxMediaCount": 20,
"media": [
{
"ref": "abc123",
"url": "https://cdn.shopify.com/…",
"originalSource": "https://…mp4",
"sources": [],
"contentType": "Video",
"width": 1080,
"height": 1920,
"upvote": 4,
"caption": "…",
"options": { "muted": true, "previewStart": 2 },
"tags": ["summer"],
"socialUser": { "username": "…", "url": "…" },
"productProfile": {
"ref": "…",
"member": { "ref": "…", "displayName": "…", "avatar": "…" },
"products": [
{ "product": { "id": "123", "handle": "tee" }, "variant": { "id": "456" } }
],
"links": [{ "type": "…", "url": "…", "title": "…", "order": 0 }],
"profileAnswers": [],
"testimonial": "…"
}
}
]
}sort is an uppercase FeedSort value. contentType is Video or Image. Moast silently drops any media that has no url, no originalSource and no sources. A video Shopify has not finished transcoding has none of these, so it is absent from the payload rather than present-and-broken.
Metafields
Owner | Namespace / key | Type | Admin access | Storefront access |
|---|---|---|---|---|
Product |
|
|
|
|
Collection |
|
|
|
|
Product |
|
|
| none declared |
Product |
|
|
|
|
$app:<widget_ref> holds a per-product or per-collection widget's baked HTML, keyed by the widget's ref. It has the same shape as a metaobject content field and the same 65,536-character cap. Moast creates a definition for it best-effort, so it is named in the admin and storefront-readable. Its absence and an empty widget are the same thing on the storefront: a widget whose bake has never run looks identical to a product with nothing tagged.
$app:feed is the legacy per-product feed JSON that the deprecated theme blocks read. Its shape is the feed data payload above plus a productId. Its definition declares admin access only. Moast's own theme app extension reads it in Liquid; do not build on it from a merchant's theme or the Storefront API. Use the proxy API or moast:schema instead.
The moast:schema metafield
moast:schema is the one surface built for outside readers: schema.org JSON-LD, an ItemList of VideoObject and ImageObject entries describing the product's Moast media.
Each entry can carry contentUrl (videos only), thumbnailUrl, uploadDate, transcript (videos only, when a transcript exists), author (the social user), about (the tagged product URLs) and an interactionStatistic counting upvotes with the LikeAction interaction type. Images omit contentUrl and transcript.
It is storefront-readable by design, which is what lets a theme render it and a crawler find it. Read it over the Storefront API with namespace: "moast", key: "schema" on the product.
Moast writes it on every product-feed sync. While the shop has the SEO integration off, the metafield holds JSON null, so null means the feature is off, not that data is coming later. A real payload requires the seo_integration feature access and the seo.ai widget setting both on.
The app proxy HTTP API
The app proxy is the read path for JavaScript. The base is /apps/moast, so the endpoints below live at /apps/moast/api/… on the store. Requests through the proxy are signed by Shopify, so no token is needed. Responses are JSON.
Read endpoints
Endpoint | Method | Returns |
|---|---|---|
| GET | All public widgets for the shop |
| GET | One widget's full render payload |
| GET | Same payload, with CORS open to any page |
| GET | A feed payload |
| GET | A product's feed payload plus settings |
| GET | The shop's storefront app settings |
| GET | Feature access for the shop |
| GET | Approved customer profiles and their media |
Replace {shop} with the store's myshopify.com domain, {ref} with a widget or feed ref, and {featureKey} with a feature key. The handlers authenticate the signed app-proxy request itself, so the shop segment identifies the request but the signature is what authorizes it.
Get one widget's payload
GET /apps/moast/api/{shop}/widget/{ref} is the main endpoint. It takes three optional query parameters:
product: a numeric product ID or product GID.collection: a numeric collection ID or collection GID.design_mode=true: marks a theme-editor request and suppresses impression recording.
Lookup order: if product is supplied, Moast reads the product's $app:<widget_ref> metafield. If no product content was found and collection is supplied, it reads the collection metafield. If neither produced content, it falls back to the widget metaobject's content field.
The response is the parsed widget, ready to use:
{
"attrs": { "class": "…", "widget": "…", "block-id": "…" },
"cssVars": { "--some-variable": "some-value" },
"media": [],
"products": [],
"settings": {
"accesses": {
"removeBranding": false,
"widgetCustomization": false,
"proAppBlocks": false,
"multiProductTagging": false,
"increasedVideoUploads": false
},
"translations": null,
"widgetSettings": null
}
}attrs and cssVars are parsed from the widget markup, media and products come from the embedded data payload, and settings holds the shop's storefront accesses, translations, and widget settings. Successful responses are cached with Cache-Control: public, max-age=300.
Error responses: { "error": "Unauthorized" } with 401, { "error": "Widget reference is required" } with 400, { "error": "Widget not found" } with 404, and { "error": "Invalid widget data" } or { "error": "Internal server error" } with 500.
Read a widget from any page (CORS)
GET /apps/moast/api/public/{shop}/widget/{ref} is for storefront JavaScript or any page outside the store domain. It takes the same product, collection and design_mode parameters and returns the same payload, but it resolves the shop from the URL and answers CORS preflights, with Access-Control-Allow-Origin: *. A shop-resolution failure is returned as { "error": "Widget not found" } with 404.
List widgets and read feeds
GET /apps/moast/api/widgets lists the shop's public widgets and responds with { "shop": "…", "widgets": [] }, cached privately for 60 seconds.
GET /apps/moast/api/{shop}/feed/{ref} reads a feed metaobject's data payload and appends a settings object. If ref is omitted, Moast uses the shop's default feed. Because feeds are legacy, this endpoint only helps on shops that still have feed rows; prefer the widget endpoint.
GET /apps/moast/api/{shop}/product-feed/{ref} reads a product's legacy $app:feed metafield. The ref is a numeric product ID or a product handle. The response is the feed payload with a settings object appended, cached for 300 seconds. If the metafield is absent or its productId does not match the requested product, the response is { "error": "Feed not found" } with 404.
Read settings and feature access
GET /apps/moast/api/{shop}/settings returns the storefront app settings: the accesses flags, translations, and widgetSettings, cached for 300 seconds.
GET /apps/moast/api/features/{featureKey}/access.json answers one feature:
{ "hasAccess": true, "reason": null, "featureKey": "some_feature" }Limited features add limit and usage, and a denied limit feature returns "hasAccess": false with "reason": "over_limit". An unknown feature key returns "hasAccess": false with "reason": "feature_not_found".
Read customer profiles
GET /apps/moast/api/profiles.json returns the shop's active, approved customer profiles with their uploaded media, plus a removeBranding flag:
{
"profiles": [],
"removeBranding": true
}Videos are only included when the shop has video view access.
Other endpoints
The proxy also carries a few endpoints that write rather than read, used by Moast's own storefront surfaces:
Endpoint | Method | Purpose |
|---|---|---|
| POST | Customer UGC submissions (form fields |
| POST | Staged upload URLs for media files |
| GET, POST | Instagram and TikTok import runs |
Reading the data in Liquid
Inside a merchant theme, the same data is available through Liquid. For a global widget, read the metaobject directly:
{{- metaobjects['app--108560121857--moast_widget']['WIDGET-REF'].content.value -}}For a per-product widget, read the product's metafield and fall back to the metaobject:
{%- assign moast_widget = product.metafields['app--108560121857']['WIDGET-REF'].value -%}
{%- unless moast_widget -%}
{%- assign moast_widget = metaobjects['app--108560121857--moast_widget']['WIDGET-REF'].content.value -%}
{%- endunless -%}
{{- moast_widget -}}Collection widgets work the same way with collection.metafields. Replace WIDGET-REF with the widget's ref. The value is the baked HTML, so you print it directly.
Feature access is also readable in Liquid, for example app.metafields.feature_access.remove_moast_branding and app.metafields.feature_access.multi_product_tagging, alongside limits such as app.metafields.feature_limit.videos_per_widget.
If you would rather not hand-write this, the Moast admin generates the embed code for you, including the script tag. For page builders, see the guides for adding Moast videos to EComposer, GemPages, and Replo.
If something is not there
A few situations look like data loss but are expected behavior:
An empty
contenton a per-product or per-collection widget is correct. The markup lives on the product's or collection's own metafield.A widget whose bake has never run and a product with nothing tagged look identical on the storefront.
Media-heavy widgets can lose media at the end because of the 65,536-character metafield cap.
Media with no
url,originalSourceandsourcesis dropped from feed payloads. A video still transcoding falls into this group.moast:schemaholdingnullmeans the SEO integration is off for the shop.
For anything else, contact Moast support and include the shop domain, the endpoint or namespace you called, and the response you got.