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
| Piece | What it is | Who adds it |
|---|---|---|
window.appbrew | The bridge. One method, postMessage, carrying a JSON envelope to the app. | The app installs it. You do nothing. |
window.appbrewSDK | A 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.
<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": []
}| Field | Type | Notes |
|---|---|---|
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. |
isLoggedIn | boolean | false when nobody is signed in, and when the app could not confirm the session. |
customerAccessToken | string | null | Shopify Storefront API customer access token. |
customer | object | null | The profile. Can be null even when isLoggedIn is true, if the app has no profile cached and the fetch returned nothing. |
customer.acceptsMarketing | boolean | Never null. Falls back to false. |
customer.defaultAddress | object | null | null when the customer has no saved address. |
tags | string[] | 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
| Symptom | Likely cause | Fix |
|---|---|---|
window.appbrewSDK is undefined | Script tag missing, or blocked by a content blocker | Confirm the tag is on the page and loads before your code |
isApp() is false inside the app | The link opened in the device browser instead of in-app | Check how that link is configured in the app |
status: 'error' after five seconds | The app is older than the method you called, or the page is not rendered by a web-view block | See version gates |
isLoggedIn: false while clearly signed in | The session could not be confirmed, or the app signs in through a method this bridge does not read | Retry once; if it persists, get in touch |
| Profile shows stale values after an edit | The app caches the profile and only refetches when its cache is empty | Reloading the page does not help: the cache is in the app, not the page. The app has to refetch |
Storefront API returns null for customer | Token expired, or your Storefront token lacks customer scopes | Check 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.

