> ## Documentation Index
> Fetch the complete documentation index at: https://docs.typewise.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Embed the chat widget in Shopify

> Add the Typewise chat widget to a Shopify theme and sign the logged-in customer's details in Liquid so they count as verified variables.

Shopify themes are rendered on Shopify's servers with Liquid, so you can add the
chat widget and sign customer variables without running a backend of your own.
This guide adds two theme snippets: a small HS256 JWT signer and the Typewise
widget embed that uses it.

> [!NOTE]
>
> You need your **client ID** and **workspace ID** from **Behavior → Channels →
> Chat → Installation**, and the signing key from [Identity
> verification](https://platform.typewise.app/channels/chat/installation/identity-verification).
> Add your store's domain to the **Domain allowlist** on the same page.

## How it works

* Shopify exposes the logged-in shopper as the `customer` object in Liquid. For
  guests it is empty.
* Liquid has a built-in `hmac_sha256` filter, so the JWT is built and signed on
  Shopify's servers. The browser only ever sees the finished token.
* The signing key lives in a theme setting, never in the page source.
* Guests get the widget without `signedVariables`. Logged-in customers get a
  token with `userId` and `user_email`. You can sign any other customer value
  the same way.

> [!WARNING]
>
> Shopify stores the key in the theme's `config/settings_data.json`. Staff with
> **Edit code** access and installed apps with theme read access can read it,
> and theme duplicates and exports carry it too. Anyone who holds the key can
> forge verified variables for any customer. Review which apps can read your
> themes, and regenerate the key in Typewise whenever staff access changes, a
> theme-reading app is removed, or a theme file is shared.

## Add the theme setting

Open **Online Store → Themes → ⋯ → Edit code**, then **Config →
`settings_schema.json`**. Add this group at the end of the array, after the last
existing group. Do not replace the file; it holds all of your theme's other
settings.

```json theme={null}
{
  "name": "Typewise",
  "settings": [
    {
      "type": "text",
      "id": "typewise_secret_key",
      "label": "Typewise secret key",
      "info": "Used to sign the logged-in customer's details for the chat widget."
    },
    {
      "type": "checkbox",
      "id": "typewise_debug",
      "label": "Typewise debug logging",
      "default": false,
      "info": "Prints masked diagnostics to the browser console. Turn off after testing."
    }
  ]
}
```

Once saved, a new **Typewise** group appears in the theme editor with two
fields: **Typewise secret key** and **Typewise debug logging**. The widget does
not sign anything until the key is filled in.

**1. Open the theme editor**

Go to **Online Store → Themes → Customize**, then click the gear icon at the
bottom of the left sidebar to open **Theme settings**.

**2. Enter the secret key**

Open the **Typewise** group and paste the `sk_...` key from [Identity
verification](https://platform.typewise.app/channels/chat/installation/identity-verification)
into **Typewise secret key**.

**3. Save**

Click **Save** at the top right. Leave **Typewise debug logging** off for
now; you turn it on later to verify the setup.

![Typewise group in Shopify theme settings](https://mintcdn.com/typewise/whSiWDm2OTNjoQEX/images/guides/channels/chat-widget/shopify/theme-settings-typewise.webp?fit=max\&auto=format\&n=whSiWDm2OTNjoQEX\&q=85\&s=622ac6d0ea6c88b6dbb94e58e6ee269d)

Theme settings are per theme. If you publish a different theme, repeat this
setup there. A duplicated or exported theme already contains the key, so treat
those copies as holding the secret.

## Add the JWT signer snippet

Under **Snippets**, choose **Add a new snippet**, name it `jwt-hs256`, and paste
the following. It takes a JSON payload string and a secret, and outputs a signed
token. The loop converts the hex output of `hmac_sha256` into the base64url form
a JWT needs.

```liquid snippets/jwt-hs256.liquid theme={null}
{%- comment -%}
  Signs a JWT with HS256. Outputs the token string.
  Args: payload_json (already a JSON object string), secret.
  Usage: {% capture token %}{% render 'jwt-hs256', payload_json: '{"a":1}', secret: settings.key %}{% endcapture %}
{%- endcomment -%}
{%- liquid
  assign header = '{"alg":"HS256","typ":"JWT"}' | base64_url_safe_encode | remove: '='
  assign payload = payload_json | base64_url_safe_encode | remove: '='
  assign input = header | append: '.' | append: payload
  assign hex = input | hmac_sha256: secret

  comment
    hmac_sha256 returns hex. JWT needs base64url of the raw bytes, so convert 3 bytes (6 hex chars) at a time.
  endcomment
  assign hexchars = '0123456789abcdef'
  assign b64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
  assign sig = ''
  for g in (0..10)
    assign start = g | times: 6
    if g == 10
      assign len = 4
    else
      assign len = 6
    endif
    assign chunk = hex | slice: start, len
    assign n = 0
    for k in (1..len)
      assign k0 = k | minus: 1
      assign c = chunk | slice: k0, 1
      assign v = hexchars | split: c | first | size
      assign n = n | times: 16 | plus: v
    endfor
    if len == 4
      assign n = n | times: 4
      assign i1 = n | divided_by: 4096 | modulo: 64
      assign i2 = n | divided_by: 64 | modulo: 64
      assign i3 = n | modulo: 64
      assign ch = b64chars | slice: i1, 1
      assign sig = sig | append: ch
      assign ch = b64chars | slice: i2, 1
      assign sig = sig | append: ch
      assign ch = b64chars | slice: i3, 1
      assign sig = sig | append: ch
    else
      assign i1 = n | divided_by: 262144 | modulo: 64
      assign i2 = n | divided_by: 4096 | modulo: 64
      assign i3 = n | divided_by: 64 | modulo: 64
      assign i4 = n | modulo: 64
      assign ch = b64chars | slice: i1, 1
      assign sig = sig | append: ch
      assign ch = b64chars | slice: i2, 1
      assign sig = sig | append: ch
      assign ch = b64chars | slice: i3, 1
      assign sig = sig | append: ch
      assign ch = b64chars | slice: i4, 1
      assign sig = sig | append: ch
    endif
  endfor
  echo input | append: '.' | append: sig
-%}
```

## Add the widget snippet

Add a second snippet named `typewise-widget`. Replace `YOUR_CLIENT_ID` and
`YOUR_WORKSPACE_ID` with your values.

```liquid snippets/typewise-widget.liquid theme={null}
{%- comment -%}
  Typewise chat widget. Signs the logged-in customer's email so the widget can trust it.
  Needs theme settings: settings.typewise_secret_key, settings.typewise_debug (optional)
  Usage in theme.liquid, before </body>: {% render 'typewise-widget' %}
{%- endcomment -%}
{%- if customer and settings.typewise_secret_key != blank -%}
  {%- liquid
    assign tw_now = 'now' | date: '%s' | plus: 0
    assign tw_exp = tw_now | plus: 604800
    assign tw_user_id = customer.id | append: '' | json
    assign tw_email = customer.email | json

    comment
      Add any other customer value here to sign it, e.g. customer.first_name.
      Declare the same name in Context Variables and it becomes a verified variable.
    endcomment
    assign tw_payload = '{"userId":' | append: tw_user_id | append: ',"user_email":' | append: tw_email | append: ',"iat":' | append: tw_now | append: ',"exp":' | append: tw_exp | append: '}'
  -%}
  {%- capture tw_token_raw -%}{%- render 'jwt-hs256', payload_json: tw_payload, secret: settings.typewise_secret_key -%}{%- endcapture -%}
  {%- liquid
    comment
      A render failure prints "Liquid error: ..." instead of a token. Only pass through a real 3-part JWT.
    endcomment
    assign tw_parts = tw_token_raw | split: '.' | size
    if tw_parts == 3
      unless tw_token_raw contains 'Liquid error'
        assign tw_token = tw_token_raw
      endunless
    endif
  -%}
{%- endif -%}
<script>
(function () {
  window.twConfig = {
    clientId: 'YOUR_CLIENT_ID',
    workspaceId: 'YOUR_WORKSPACE_ID',
    {%- if tw_token != blank %}
    signedVariables: {{ tw_token | json }},
    {%- endif %}
    variables: {},
  };
  var s = document.createElement('script');
  s.type = 'module';
  s.src = 'https://platform.typewise.app/sdk/sdk.js';
  document.head.appendChild(s);
})();
</script>
```

The token expires 7 days after the page was rendered. Shopify's page cache can
serve a page rendered a few minutes earlier, which is well inside that window.

> [!TIP]
>
> `user_email` is pre-configured in [Context
> Variables](https://docs.typewise.app/documentation/settings/context-variables.md). To sign more values,
> add them to `tw_payload` in the snippet, for example `customer.first_name` or
> `customer.tags`, and declare the same variable name in Context Variables.
> Anything signed this way is a verified variable and can be used as a secure
> input on actions and lookups. Variables that are not declared are dropped on
> arrival.

## Render it in the theme layout

Open **Layout → `theme.liquid`** and add one line just before `</body>`:

```liquid layout/theme.liquid theme={null}
{% render 'typewise-widget' %}
```

Save, then open the storefront. The chat bubble appears for every visitor. Log
in as a customer through the store's account page to test the signed path. Being
logged into the Shopify admin does not create a storefront customer session.

> [!NOTE]
>
> Checkout and the order status page are not rendered by your theme, so the
> widget does not appear there. That requires a Shopify app with a theme app
> extension.

## Verify the setup

Turn on **Typewise debug logging** in theme settings and add the block below to
the end of `typewise-widget`. It prints one JSON object to the browser console
under `[typewise-debug]` with presence checks and masked values only. It never
prints the key, the email, the names or the token.

> **Debug block for typewise-widget.liquid**
>
> ```liquid theme={null}
> {%- if settings.typewise_debug -%}
> {%- comment -%}
>   Debug output. Only presence, lengths and masked values are printed, never the key, email or token.
> {%- endcomment -%}
> {%- liquid
>   assign dbg_key_len = settings.typewise_secret_key | default: '' | size
>   assign dbg_email_masked = ''
>   if customer.email != blank
>     assign dbg_parts = customer.email | split: '@'
>     assign dbg_email_masked = dbg_parts[0] | slice: 0, 1 | append: '***@' | append: dbg_parts[1]
>   endif
>   assign dbg_first_len = customer.first_name | default: '' | size
>   assign dbg_last_len = customer.last_name | default: '' | size
>   assign dbg_token_len = tw_token | default: '' | size
>   assign dbg_render_error = ''
>   if tw_token_raw contains 'Liquid error'
>     assign dbg_render_error = tw_token_raw | strip
>   endif
> -%}
> <script>
> (function () {
>   var d = {
>     key_present: {{ dbg_key_len | json }} > 0,
>     key_length: {{ dbg_key_len | json }},
>     customer_logged_in: {% if customer %}true{% else %}false{% endif %},
>     email_present: {{ dbg_email_masked | json }} !== '',
>     email_masked: {{ dbg_email_masked | json }},
>     first_name_present: {{ dbg_first_len | json }} > 0,
>     last_name_present: {{ dbg_last_len | json }} > 0,
>     token_present: {{ dbg_token_len | json }} > 0,
>     token_length: {{ dbg_token_len | json }},
>     render_error: {{ dbg_render_error | json }} !== '',
>     render_error_message: {{ dbg_render_error | json }}
>   };
>   // Decode the payload (not secret) to confirm the token is well formed and not expired.
>   var t = window.twConfig && window.twConfig.signedVariables;
>   if (t) {
>     try {
>       var b64 = t.split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
>       var p = JSON.parse(decodeURIComponent(escape(atob(b64))));
>       var now = Math.floor(Date.now() / 1000);
>       d.payload_email_matches = typeof p.user_email === 'string' && p.user_email.charAt(0) === d.email_masked.charAt(0) && p.user_email.split('@')[1] === d.email_masked.split('@')[1];
>       d.payload_expires_in_days = Math.round((p.exp - now) / 86400);
>       d.payload_not_expired = p.exp > now;
>     } catch (e) {
>       d.payload_decode_error = String(e && e.message);
>     }
>   }
>   d.overall_ok = d.key_present && d.customer_logged_in && d.email_present && d.token_present && !d.render_error && d.payload_not_expired === true;
>   console.log('[typewise-debug]', JSON.stringify(d, null, 2));
> })();
> </script>
> {%- endif -%}
> ```

A working setup for a logged-in customer logs `overall_ok: true`. What to check
when it does not:

| Field | Meaning when false |
| :- | :- |
| `key_present` | Theme setting is missing or empty. `key_length` should match your key. |
| `customer_logged_in` | No storefront customer session. Log in through the store's account page. |
| `token_present` | Both of the above passed, but signing did not produce a JWT. If `render_error` is true, `render_error_message` says why, usually a missing `jwt-hs256` snippet. |
| `payload_not_expired` | The token's `exp` is in the past. Check the store's clock and cache settings. |

Turn debug logging off once you are done.

## Troubleshooting

* **The chat shows "An error occurred" on the first message.** The page sent
  `signedVariables: null`. The widget snippet above omits the key when there is
  no token; make sure you did not change that branch.
* **The widget loads but variables are untrusted.** The key in theme settings
  does not match the current key in Typewise, or the token has expired. Copy the
  key again from Identity verification.
* **Nothing renders.** Check that your store's domain is on the Domain allowlist
  and that the `render` line sits inside `<body>`.

## See also

* [Chat identity verification](https://docs.typewise.app/guides/channels/chat-widget/identity-verification.md): the
  signing model and backend examples for non-Shopify sites
* [Context Variables](https://docs.typewise.app/documentation/settings/context-variables.md): declare
  verified variables and map secure inputs
* [Chat configuration](https://docs.typewise.app/documentation/behavior/chat.md): installation and domain
  allowlist


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.