# The Web SDK (/docs/build/web-sdk)



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 [#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](/docs/build/webview-contract).

## Add the SDK [#add-the-sdk]

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

```html title="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 [#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.

```js
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:

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

<Callout type="warn" title="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.
</Callout>

## Read the signed-in customer [#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.

```js
const result = await window.appbrewSDK.getUserDetails()
```

Signed in:

```json
{
  "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:

```json
{
  "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 [#a-worked-example]

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

```html
<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 [#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.

```js
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.

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

<Callout type="warn" title="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](/docs/start/getting-help) about that separately.
</Callout>

### Passing the token in without JavaScript [#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 [#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.

```js
// 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.

```js
window.appbrewSDK.goBackInApp()
```

Every `kind` the app accepts is listed in the [Web SDK reference](/docs/reference/web-sdk#link-kinds).

## Add to the cart [#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.

```js
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:

```js
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 [#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.

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

## Handle the four outcomes [#handle-the-four-outcomes]

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

```js
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.
}
```

<Callout title="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.
</Callout>

## Security [#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.

<Callout type="warn" title="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.
</Callout>

## Troubleshooting [#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](/docs/build/webview-contract#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](/docs/start/getting-help)                                                                                                                                                                                                |
| 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](https://solutions.appbrew.tech/web-sdk/appbrew-sdk-sample). Open it in an app to see live payloads.
