Block Client API

Block Client API

Block Client is a one-way callback API for events raised by Bubblehouse blocks. Calls identify their originating blocks by the instance parameter supplied in the block URL.

Website usage

On websites, Bubblehouse blocks typically run inside IFRAMEs, and this API operates cross-origin through window.parent.postMessage.

Do you need to worry about Block Client calls on the web?

Bubblehouse supplies the bubblehouse.js script, which most clients add to their sites. On some supported ecommerce integrations, such as Shopify, a theme extension adds it automatically.

This script handles many Block Client calls:

  • Resize1 for resizing embedded Bubblehouse IFRAMEs to content height
  • OpenPopup1 for launching Bubblehouse popups
  • ClosePopup1 for handling the close button inside popups

On supported ecommerce platforms with a first-party cart API, the script also handles:

This means that, as long as you include our script, you don’t need to worry about most Block Client calls. During technical integration sessions, we will tell you whether you need to handle other calls, such as ApplyDiscountCode1, yourself.

Handling Block Client calls on the web

Use window.addEventListener in your frontend JavaScript code, and read event.data?.bubblehouseBlockClientCall to recognize Bubblehouse Block Client calls.

Example (web)

window.addEventListener('message', async function(ev) {
  const call = ev.data?.bubblehouseBlockClientCall
  if (!call) {
    return
  }

  switch (call) {
    case 'ApplyDiscountCode1':
      applyDiscountCode(ev.data.code)
      break
    case 'AddProductToCart1': {
      const variantID = ev.data.variantId
      if (typeof variantID !== 'string' || !variantID) {
        break
      }

      if (!await addProductToCart(variantID)) {
        break
      }

      const message = ev.data.successMessage
      const cartURL = ev.data.cartUrl
      if (typeof message === 'string' && message) {
        showMessage(message)
      } else {
        window.location.assign(cartURL || '/cart')
      }
      break
    }
  }
}, false)

Replace applyDiscountCode, addProductToCart, and showMessage with your ecommerce integration. addProductToCart should return true only after a successful add. successMessage takes precedence over cartUrl. When cartUrl is absent or empty, open /cart.

Native app usage

A native app loads Bubblehouse blocks in a web view. The app can receive supported callbacks through a custom URL scheme.

Make sure you load the block through its canonical URL with env=mobile:

https://app.bubblehouse.com/s/SHOP_SLUG/blocks/Rewards7
  ?instance=UNIQUE_ID
  &auth=CUSTOMER_TOKEN
  &standalone=1
  &env=mobile
  &...

Whitespace and newlines in URL examples are illustrative.

Replace SHOP_SLUG with the Bubblehouse shop slug supplied for your integration, UNIQUE_ID with the block instance, and CUSTOMER_TOKEN with the current customer’s token.

Do you need to worry about Block Client calls in mobile apps?

You do NOT need to handle Resize1, OpenPopup1, or ClosePopup1; a top-level loyalty block does not need IFRAME resizing and renders its popups internally.

However, you might want to handle:

Handling Block Client calls from mobile apps

Bubblehouse sends a supported callback by navigating to a custom URL scheme; we call this “native callback delivery”:

<scheme>://bubblehouse/blockclient/<CallName>?<percent-encoded-key>=<percent-encoded-JSON-value>&...

For example:

yourschemanamehere://bubblehouse/blockclient/AddProductToCart1
  ?instance=%22bubblehouse-rewards%22
  &options=null
  &quantity=1
  &variantId=%2256718802518391%22

Ask Bubblehouse’s configuration team to set Block Client URL scheme. The app must register the same scheme on each supported platform. env=mobile enables native callback delivery if a URL scheme is configured.

Each message field becomes a separate query parameter. Bubblehouse JSON-encodes each value, then percent-encodes the key and encoded value. The path supplies the call name, so the params exclude bubblehouseBlockClientCall key.

Bubblehouse delivers via postMessage and via URL scheme (if configured and env=mobile) independently. A block in an iframe posts the call to its parent. A block with a configured custom URL scheme and env=mobile also navigates to that scheme, whether it is top-level or framed. If both are available, Bubblehouse does both. If neither is available, Bubblehouse ignores the call.

The app receives the URL through the web container’s normal navigation handling:

  • WKNavigationDelegate for WKWebView
  • the app’s URL-open handler for SFSafariViewController
  • shouldOverrideUrlLoading for Android WebView

Each example below waits for addProductToCart to report a successful add before showing a message or redirecting.

Replace STOREFRONT_URL with your online store’s base URL.

Example (iOS WKWebView)

Decode the callback URL in a WKNavigationDelegate, cancel the navigation, and handle the call:

struct BlockClientCall {
    let name: String
    let values: [String: Any]

    init?(url: URL) {
        guard url.scheme?.lowercased() == "yourschemanamehere",
              url.host?.lowercased() == "bubblehouse" else {
            return nil
        }

        let path = url.path.split(separator: "/")
        guard path.count == 2, path[0] == "blockclient",
              let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
            return nil
        }

        var values: [String: Any] = [:]
        for item in components.queryItems ?? [] {
            guard let json = item.value?.data(using: .utf8),
                  let value = try? JSONSerialization.jsonObject(
                      with: json,
                      options: .fragmentsAllowed
                  ) else {
                return nil
            }
            values[item.name] = value
        }

        self.name = String(path[1])
        self.values = values
    }
}

final class RewardsNavigationDelegate: NSObject, WKNavigationDelegate {
    func webView(
        _ webView: WKWebView,
        decidePolicyFor navigationAction: WKNavigationAction,
        decisionHandler: @escaping @MainActor @Sendable (
            WKNavigationActionPolicy
        ) -> Void
    ) {
        guard let url = navigationAction.request.url,
              let call = BlockClientCall(url: url) else {
            decisionHandler(.allow)
            return
        }

        decisionHandler(.cancel)
        switch call.name {
        case "ApplyDiscountCode1":
            if let code = call.values["code"] as? String {
                applyDiscountCode(code)
            }
        case "AddProductToCart1":
            guard let variantID = call.values["variantId"] as? String,
                  !variantID.isEmpty else {
                break
            }

            Task {
                guard await addProductToCart(variantID: variantID) else {
                    return
                }

                if let message = call.values["successMessage"] as? String,
                   !message.isEmpty {
                    showMessage(message)
                } else {
                    let cartURL = call.values["cartUrl"] as? String ?? ""
                    let storefrontURL = URL(string: "STOREFRONT_URL")!
                    let url = URL(
                        string: cartURL.isEmpty ? "/cart" : cartURL,
                        relativeTo: storefrontURL
                    )!.absoluteURL
                    webView.load(URLRequest(url: url))
                    // or navigate to the native cart instead
                }
            }
        default:
            break
        }
    }
}

Replace applyDiscountCode, addProductToCart, and showMessage with your app’s cart integrations.

Example (iOS SFSafariViewController)

SFSafariViewController does not expose navigation policy callbacks. Register the scheme in the app’s URL Types and receive callbacks through the scene URL-open lifecycle. This example uses the BlockClientCall(url:) initializer from the WKWebView example above:

extension SceneDelegate {
    func scene(
        _ scene: UIScene,
        openURLContexts URLContexts: Set<UIOpenURLContext>
    ) {
        for context in URLContexts {
            guard let call = BlockClientCall(url: context.url) else {
                continue
            }

            switch call.name {
            case "ApplyDiscountCode1":
                if let code = call.values["code"] as? String {
                    applyDiscountCode(code)
                }
            case "AddProductToCart1":
                guard let variantID = call.values["variantId"] as? String,
                      !variantID.isEmpty else {
                    break
                }

                Task {
                    guard await addProductToCart(variantID: variantID) else {
                        return
                    }

                    if let message = call.values["successMessage"] as? String,
                       !message.isEmpty {
                        showMessage(message)
                    } else {
                        let cartURL =
                            call.values["cartUrl"] as? String ?? ""
                        let storefrontURL = URL(string: "STOREFRONT_URL")!
                        let url = URL(
                            string: cartURL.isEmpty ? "/cart" : cartURL,
                            relativeTo: storefrontURL
                        )!.absoluteURL
                        await UIApplication.shared.open(url)
                        // or navigate to the native cart instead
                    }
                }
            default:
                break
            }
        }
    }
}

Apps without scenes can decode the URL in application(_:open:options:) instead.

Example (Android WebView)

Decode query values with JSONTokener, then consume matching navigation in shouldOverrideUrlLoading:

data class BlockClientCall(
    val name: String,
    val values: JSONObject,
) {
    companion object {
        fun from(uri: Uri): BlockClientCall? {
            if (!uri.scheme.equals("yourschemanamehere", ignoreCase = true) ||
                !uri.host.equals("bubblehouse", ignoreCase = true)) {
                return null
            }

            val path = uri.pathSegments
            if (path.size != 2 || path[0] != "blockclient") {
                return null
            }

            val values = JSONObject()
            for (key in uri.queryParameterNames) {
                val json = uri.getQueryParameter(key) ?: return null
                val value = try {
                    JSONTokener(json).nextValue()
                } catch (_: JSONException) {
                    return null
                }
                values.put(key, value)
            }

            return BlockClientCall(path[1], values)
        }
    }
}

webView.webViewClient = object : WebViewClient() {
    override fun shouldOverrideUrlLoading(
        view: WebView,
        request: WebResourceRequest,
    ): Boolean {
        val call = BlockClientCall.from(request.url) ?: return false
        when (call.name) {
            "ApplyDiscountCode1" -> applyDiscountCode(call.values.getString("code"))
            "AddProductToCart1" -> {
                val variantID = call.values.opt("variantId") as? String ?: return true
                if (variantID.isEmpty()) return true

                addProductToCart(variantID = variantID) { added ->
                    if (!added) return@addProductToCart

                    val message = call.values.opt("successMessage") as? String
                    val cartURL =
                        (call.values.opt("cartUrl") as? String)
                            .orEmpty()
                            .ifEmpty { "/cart" }
                    view.post {
                        if (!message.isNullOrEmpty()) {
                            showMessage(message)
                        } else {
                            view.loadUrl(
                                URL(URL("STOREFRONT_URL"), cartURL).toString()
                            )
                            // or navigate to the native cart instead
                        }
                    }
                }
            }
        }
        return true
    }
}

Replace applyDiscountCode, addProductToCart, and showMessage with your app’s cart integrations. The WebResourceRequest overload requires Android API 24 or later; use the deprecated string overload as well if your app supports older versions.

Returning values

This is a one-way API: currently there’s no provision to return a response.

Authentication & Security

Block Client adds no authentication; customer authentication happens separately inside the displayed block.

Note, however, that any iframe on your page can fake a Block Client API call, including untrusted code like ads. Built-in Bubblehouse handler verifies the origin of incoming messages. The calls that you would typically handle yourself are safe under Bubblehouse threat model and security policy; the worst they allow is for a subframe to display a message or redirect to another URL. Worth keeping this in mind and doing your own analysis if your page hosts particularly untrusted content.

Previous
Widget4