AppbrewDevelopers
Build

The Web SDK

Call the app from a web page opened inside it.

When one of your web pages opens inside an Appbrew app, it can talk to the app. It can read the signed-in customer, add to the cart, send the shopper to a native screen, and forward analytics events. The Web SDK is the JavaScript you add to that page to make those calls.

This page is for whoever owns the web page: a Shopify theme, a landing page, or a size-guide or loyalty page you host yourself.

The two pieces

PieceWhat it isWho adds it
window.appbrewThe bridge. One method, postMessage, carrying a JSON envelope to the app.The app installs it. You do nothing.
window.appbrewSDKA wrapper over the bridge: promises, timeouts, and request matching.You, with a script tag.

Everything on this page uses appbrewSDK. The envelope underneath it is described in the webview contract.

Add the SDK

One script tag, ahead of any code that calls it.

theme.liquid
<script src="https://solutions.appbrew.tech/web-sdk/appbrew-sdk.js"></script>

It is safe on every page. In a desktop or mobile browser it loads, finds no bridge, and reports that it is not running inside the app. It defines window.appbrewSDK, and also leaves a helper named getRequestId on window, so avoid that name in your own code.

Check you are inside the app

isApp() returns true when the bridge is present. Guard every other call with it, and fall back to whatever your page normally does.

if (window.appbrewSDK?.isApp()) {
  // running inside an Appbrew app
}

The app also sets a user agent containing Appbrew/<appname>/<ios|android>/<build>, for example Appbrew/acmestore/ios/412. Test for that with a substring check when you need the answer before the SDK script has loaded, or on the server:

<script>
  if (navigator.userAgent.includes('Appbrew/')) {
    document.documentElement.classList.add('in-appbrew-app')
  }
</script>

Match the prefix, not the whole string

On iOS, one of the two webview implementations replaces the browser user agent outright rather than appending to it, so the usual Mozilla/5.0 … tokens are not guaranteed to be there. Check for Appbrew/ as a substring and nothing else.

Read the signed-in customer

getUserDetails() asks the app who is signed in. It returns a promise that resolves within five seconds, whatever happens, so you never need a try/catch around it.

const result = await window.appbrewSDK.getUserDetails()

Signed in:

{
  "status": "success",
  "isLoggedIn": true,
  "customerAccessToken": "b3f1c8e2a94d7...",
  "customer": {
    "id": "gid://shopify/Customer/1234567890",
    "email": "customer@example.com",
    "firstName": "Alex",
    "lastName": "Doe",
    "phone": "+919876543210",
    "displayName": "Alex Doe",
    "acceptsMarketing": true,
    "defaultAddress": {
      "id": "gid://shopify/MailingAddress/987654321",
      "address1": "12 Residency Road",
      "address2": null,
      "city": "Bengaluru",
      "province": "Karnataka",
      "country": "India",
      "countryCodeV2": "IN",
      "zip": "560025",
      "firstName": "Alex",
      "lastName": "Doe",
      "phone": "+919876543210"
    }
  },
  "tags": ["vip", "wholesale"]
}

Signed out:

{
  "status": "success",
  "isLoggedIn": false,
  "customerAccessToken": null,
  "customer": null,
  "tags": []
}
FieldTypeNotes
status'success' | 'error'The app only ever sends 'success'. An 'error' value is written by the SDK itself when nothing came back in five seconds.
isLoggedInbooleanfalse when nobody is signed in, and when the app could not confirm the session.
customerAccessTokenstring | nullShopify Storefront API customer access token.
customerobject | nullThe profile. Can be null even when isLoggedIn is true, if the app has no profile cached and the fetch returned nothing.
customer.acceptsMarketingbooleanNever null. Falls back to false.
customer.defaultAddressobject | nullnull when the customer has no saved address.
tagsstring[]Shopify customer tags. Empty array when there are none.

Every other field on customer and defaultAddress is string | null. A customer may have no phone, no last name, or no saved address, so render with a fallback.

A worked example

Greet the customer, reveal tag-gated content, prefill a form, and show a sign-in button to everyone else.

<div id="greeting" hidden></div>
<div id="vip-banner" hidden>Your VIP pricing is applied at checkout.</div>
<button id="sign-in-cta" hidden>Sign in to see your account</button>

<script>
;(async function () {
  if (!window.appbrewSDK?.isApp()) return

  const result = await window.appbrewSDK.getUserDetails()
  if (!result || result.status !== 'success') return

  if (!result.isLoggedIn) {
    const cta = document.getElementById('sign-in-cta')
    cta.hidden = false
    cta.addEventListener('click', function () {
      window.appbrewSDK.gotoLink({ kind: 'screen', value: 'default-signin' })
    })
    return
  }

  const customer = result.customer || {}
  const greeting = document.getElementById('greeting')
  greeting.textContent = 'Hi ' + (customer.firstName || customer.displayName || 'there')
  greeting.hidden = false

  if (result.tags.includes('vip')) {
    document.getElementById('vip-banner').hidden = false
  }

  const email = document.querySelector('#newsletter-email')
  if (email && customer.email) email.value = customer.email
})()
</script>

Use the access token

customerAccessToken is a Shopify Storefront API customer access token. Use it for authenticated Storefront calls from your page: order history, saved addresses, customer metafields.

You also need a Storefront API public access token for the shop, created in Shopify admin under Settings → Apps and sales channels → Develop apps, with the customer scopes you need. That one is shop-level and safe in theme code. The customer access token is not.

async function fetchOrders(customerAccessToken) {
  const response = await fetch(
    'https://' + window.Shopify.shop + '/api/2025-04/graphql.json',
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Shopify-Storefront-Access-Token': 'YOUR_PUBLIC_STOREFRONT_TOKEN',
      },
      body: JSON.stringify({
        query: `
          query GetOrders($token: String!) {
            customer(customerAccessToken: $token) {
              orders(first: 10, reverse: true) {
                edges {
                  node {
                    id
                    orderNumber
                    processedAt
                    fulfillmentStatus
                    totalPrice { amount currencyCode }
                  }
                }
              }
            }
          }
        `,
        variables: { token: customerAccessToken },
      }),
    }
  )
  const json = await response.json()
  return json?.data?.customer?.orders?.edges?.map((e) => e.node) ?? []
}

To send identity to a backend of your own, put the token in a header over HTTPS and verify it server-side by calling the Storefront API with it. A customer id or email sent from a browser proves nothing on its own.

await fetch('https://your-api.example.com/loyalty/balance', {
  headers: { 'X-Customer-Access-Token': result.customerAccessToken },
})

The token is not a theme session

It is an API credential. With a valid token in hand, {{ customer }} in Liquid is still empty, customer_logged_in is still false, and /account still redirects to the login page. Use it for personalisation and for your own API calls. If you need Shopify's own account pages to work inside the app, talk to us about that separately.

Passing the token in without JavaScript

A web-view block can template the token straight into the URL it loads. Put {{customerAccessToken}} anywhere in the block's configured link and the app substitutes it before navigating:

https://example.com/loyalty?token={{customerAccessToken}}

Only the first occurrence is substituted. Nothing is substituted when no one is signed in, so the placeholder reaches your server verbatim. Treat that as the signed-out case. Prefer getUserDetails() where you can: a token in a URL lands in server logs and Referer headers.

Move the shopper into the app

gotoLink(link) hands navigation to the app. It does not wait for the app to confirm, so there is nothing useful to await.

// The app's own sign-in screen, wherever this app puts it
window.appbrewSDK.gotoLink({ kind: 'screen', value: 'default-signin' })

// A product, by handle
window.appbrewSDK.gotoLink({ kind: 'product', value: 'your-product-handle' })

// A collection, by handle
window.appbrewSDK.gotoLink({ kind: 'collection', value: 'best-sellers' })

// The cart
window.appbrewSDK.gotoLink({ kind: 'cart', value: 'cart' })

goBackInApp() closes the page and returns to the previous screen.

window.appbrewSDK.goBackInApp()

Every kind the app accepts is listed in the Web SDK reference.

Add to the cart

addToCart takes a product handle, a variant id, and a quantity. The handle is sent along but the app ignores it: items are added by variant.

const result = await window.appbrewSDK.addToCart(
  'product-handle',
  'gid://shopify/ProductVariant/1234567890',
  1
)
// { status: 'success', message: 'Item added to cart' }

For several items in one go, addMultipleItemsToCart takes an array and adds them in a single cart update:

await window.appbrewSDK.addMultipleItemsToCart([
  { handle: 'tee', variantId: 'gid://shopify/ProductVariant/111', quantity: 2 },
  { handle: 'cap', variantId: 'gid://shopify/ProductVariant/222', quantity: 1 },
])

Both resolve to { status: 'error' } if the app has not confirmed within five seconds. Neither answer is proof. A rejected or out-of-stock variant produces silence, and therefore a timeout; an error inside the app still comes back as 'success'. Re-read the cart rather than trusting the result.

Forward an analytics event

sendEvent(name, data) passes an event to every analytics tracker the app has configured. Like gotoLink, it does not wait for a reply.

window.appbrewSDK.sendEvent('viewed_loyalty_page', { tier: 'gold' })

Handle the four outcomes

The call never throws. There are four cases, and they want different responses.

const result = await window.appbrewSDK.getUserDetails()

if (!result) {
  // No bridge at all. The page is not running inside the app.
} else if (result.status === 'error') {
  // Sent, but nothing came back within five seconds.
} else if (!result.isLoggedIn) {
  // Nobody signed in. Show a sign-in call to action.
} else {
  // Signed in. result.customer and result.tags are ready.
}

Do not treat false as a sign-out

isLoggedIn: false also comes back when the app briefly could not confirm the session, on a poor network for instance. Show the sign-in prompt, and leave your own stored state alone.

Security

The customer access token acts on behalf of a real shopper. Handle it the way you would handle a password.

Do:

  • Call getUserDetails() only on pages whose scripts you control.
  • Keep the token in a local JavaScript variable, scoped to the page.
  • Send it only to Shopify or to your own HTTPS backend.
  • Cache non-sensitive fields only, such as name or tags, and only in sessionStorage.

Do not:

  • Put the token in a URL, query string, or redirect.
  • Write it to localStorage, a cookie, or a hidden form field.
  • Log it to the console or to an analytics tool.
  • Pass it to third-party scripts unless that vendor documents support for Shopify customer access tokens.

The reply is a page-wide event

The app delivers responses with window.postMessage, which any script on the page can observe. On a theme loading many third-party apps, call getUserDetails() where the script surface is under your control, and never rebroadcast the payload yourself.

Troubleshooting

SymptomLikely causeFix
window.appbrewSDK is undefinedScript tag missing, or blocked by a content blockerConfirm the tag is on the page and loads before your code
isApp() is false inside the appThe link opened in the device browser instead of in-appCheck how that link is configured in the app
status: 'error' after five secondsThe app is older than the method you called, or the page is not rendered by a web-view blockSee version gates
isLoggedIn: false while clearly signed inThe session could not be confirmed, or the app signs in through a method this bridge does not readRetry once; if it persists, get in touch
Profile shows stale values after an editThe app caches the profile and only refetches when its cache is emptyReloading the page does not help: the cache is in the app, not the page. The app has to refetch
Storefront API returns null for customerToken expired, or your Storefront token lacks customer scopesCheck your Storefront token's scopes first. Calling getUserDetails() again will not help: while the app holds a cached profile it re-reads the same token rather than renewing it, so an expired token comes back expired. Send the shopper through sign-in again

A page exercising every method is published at solutions.appbrew.tech/web-sdk/appbrew-sdk-sample. Open it in an app to see live payloads.

On this page