Integrations

Analytics and pixels, cookie consent, Stripe checkout, building a shop, form webhooks.

This is the guide the PageHub MCP server gives an AI agent for get_style_reference({ topic: "integrations" }). Names like set_theme are its tools (connect one), and the liquid topic is the HTML + Liquid reference.
On this page

Privacy: privacy: { consentMode, privacyUrl, builtInAnalytics, submissionRetentionDays }.

  • consentMode: off, eu (EU and UK visitors) or all. When on, the tags below wait for Accept. A page with the cookie-consent block asks through it, so design or translate that block for a custom banner; otherwise visitors get a plain English one. A footer link with data-cookie-settings lets them change their answer. Scripts you write yourself must check localStorage["ph-cookie-consent"].
  • privacyUrl: the site's privacy page, linked from the plain banner.
  • builtInAnalytics: false removes PageHub's own analytics.
  • submissionRetentionDays: 0, 30, 90, 180 or 365. Older form submissions are deleted nightly. Ask the owner first: they can't be recovered.

Providers (rendered as real tags on published pages):

  • googleAnalytics: GA4 id G-XXXXXXXXXX (Admin → Data Streams).
  • googleTagManager: GTM-XXXXXXX. With GTM, configure tags inside GTM and pass only the container id here.
  • googleSearchConsole: the verification content value, not the whole meta tag.
  • metaPixel: Meta Pixel id.
  • googleAds: AW-XXXXXXXXXX. Shares the gtag.js loader with GA4.

Per-action events: data-track="ga4:<event>" on a link or button reports it. GA4 and Meta wire automatically once their ids are set.

Anything else (chat widgets, A/B testing, CRMs, custom scripts): site.json inject head / footer — third-party snippets only. Per-page head code: a tag in that page file's <head>.

Stripe

stripe_connect returns a single-use onboarding URL while setup is unfinished. Only the site owner can complete it: hand them the URL, then call stripe_connect again once they're done to confirm ready. set_checkout and the stripe_* lookups need that ready connection.

Once Stripe is ready, the site's Stripe products sync into a products collection on their own (already sellable — no set_checkout needed). The first sync runs in the background, so if list_collections doesn't show it yet, or products changed in Stripe since, call refresh_products (returns the product count).

set_checkout makes a collection sellable: data-cart="add" buttons on its rows then charge through Stripe. Without priceIdField each row charges its numeric price field in minor units (2500 = $25.00), so the collection needs that number field; with priceIdField each row holds a Stripe price id. remove: true stops selling; the rows stay.

Orders and bookings: list_orders (paid, refunds, booking, balance, deposit hold, agreement, shipping) and get_order. update_order ships, refunds, approves a held order, charges a stay's balance, places / keeps / releases a deposit hold, unbooks dates (never refunds) or resends a link. It moves the owner's money: only for an order and amount they asked about, with confirmAmount set to what they agreed.

Build a shop

Fastest: create_site({ template: "modern-store" }) (apparel: search, facet filters, variants) or "moss-and-fern" (plant shop + a care-guide collection that ships with it), then stripe_connect. The owner's own products fill the shop. To build one, these are the parts:

  • Shop page: a URL-driven listing, not a bare loop. {% assign list = query["products?limit=12&in=q,category,sort,page,minPrice,maxPrice"] %} and the identical key in query_meta[...] (totalCount, totalPages, page) give this visitor's rows and pager; {% for item in list %} draws the cards. The filters are a <form method="get" data-autosubmit="300ms"> with inputs named q and sort (newest, price_asc, price_desc, alpha) and category chips as links ?category=Tees (names from product_categories; size / colour counts from product_facets, selected as ?facet.color=red, so add facet.* to in=). Start from the list-url-filters block and the connector-product-grid card (its data-cart-item JSON is what the cart reads). To filter without a reload give the grid an id and data-refetch="q,category,sort,page". Full syntax: get_style_reference({ topic: "liquid" }), "Lists from the URL".
  • Filter on product row fields (categories, tags, subcategory, optionNamesCsv), never Stripe metadata — the sync doesn't copy metadata, so a metadata filter matches nothing.
  • Product page: page pathPattern: ":handle", the row matched by request.params.handle. Add to Cart is a data-cart="add" button; data-cart="open" opens the cart, data-cart-count shows the count.
  • Order confirmed page and the account page (magic-link sign-in, profile and order history from customer): start from the library blocks order-confirmed, checkout-cancelled-banner and customer-account. Customer accounts need a paid plan.
  • Shipping rates, countries and Stripe Tax are the owner's call, set on the dashboard's Shop page — ask, don't invent rates.
  • Test on the preview (the previewUrl that create_site, duplicate_site and get_import_status return, or screenshot_site: the draft with its scripts running) and again after publishing. Never build a preview link by hand: previews live on per-site hosts.

Form inbox and webhooks

list_form_submissions is the inbox (each submission's webhook delivery: delivered, retrying or FAILED). A {% form webhook: 'https://…' %} URL goes live on publish; list_form_webhooks lists them with the endpoint key set_form_webhook_secret takes (rotate: true shows the new secret once; set stores the receiver's). After the receiver is fixed, resend_form_webhook replays a submission with the same submissionId. Owners only. Body, signature and retries: the liquid topic, Forms.

Site emails

After update_site_email, run preview_site_email: an email with errors sends the default instead, and the preview is where those errors show.