Integration

This page is the consumer guide for embedding mas-consents from another application. It covers the URL contract, host messaging, backend calls triggered by the component, and the minimum listener a host should implement.

Embedding targets

mas-consents can be embedded in:

  • a browser iframe
  • a browser opener/popup flow
  • React Native WebView
  • iOS WKWebView
  • Android WebView

URL parameters

The frontend reads the query string once during startup. For real backend calls, the host must provide enough context for the consents API.

ParameterRuntime behaviorConsumer expectation
tokenForwarded as Authorization: Bearer <token> when presentRequired for real backend calls
customerIdUsed in GET/POST pathsRequired for real backend calls
brandUsed in GET/POST paths. mock enables local mock behaviorRequired for real backend calls
sectorForwarded as x-sector headerRequired for real backend calls
viewRepeated params are preserved and sent as repeated query paramsRequired by most host flows
langNormalized against the supported locale list. Falls back to es when missing or invalidRecommended
localesOptional comma-separated list. Defaults to all supported locales. Invalid entries are discardedOptional
choiceOptional filter passed to the backendOptional
actorIdIncluded in POST body as actor_idExpected for real submit flows
sellChannelIncluded in POST body as sell_channelExpected for real submit flows
traceIdIncluded in POST body as trace_idExpected for tracing and submit flows
enableCustomConfigOnly exact value true has an effect. It makes the component wait for host JSON configurationOptional
hostCommunicationopener enables the browser opener communication modeOptional
hostOriginRequired with hostCommunication=opener; invalid origins are ignoredOptional unless opener mode is used

Supported locales:

  • es
  • eu
  • ca
  • en
  • gl

Supported view values:

  • MANDATORY
  • PORTFOLIO
  • SGDA
  • INTERNAL
  • PRIVATE
  • WHATSAPP
  • OGW_DEVICE_SWAP
  • OGW_SIM-SWAP
  • OGW_NUMBER-VERIFICATION
  • OGW_KYC-MATCH

Multiple views are passed as repeated params:

text

?view=MANDATORY&view=PORTFOLIO

Supported choice values:

  • PENDING
  • REJECTED
  • ACCEPTED

Query string cleanup

The component reads the URL params during bootstrap and then clears the query string from the browser URL. The only exception is brand=mock, which keeps the query string visible for local debugging.

Host messaging

The bridge emits structured messages and also keeps plain string browser events for compatibility. New integrations should support structured messages.

Outbound structured messages

Browser iframe hosts receive structured messages as objects:

json

{
  "source": "consents",
  "version": "v1",
  "type": "CONSENTS_LOADED",
  "payload": {}
}

React Native, Android WebView, iOS WebView and browser opener flows receive the same payload serialized as JSON text. Hosts in those environments should parse the received string before reading source or type.

Outbound message types:

  • CONSENTS_CONFIG_REQUIRED: emitted only when enableCustomConfig=true
  • CONSENTS_LOADED: emitted after consents are loaded
  • CONSENTS_COMPLETED: emitted after a successful submit or when the backend says the campaign is already complete
  • CONSENTS_ERROR: emitted with backend error payload when loading or submitting fails

Compatibility string events

Browser iframe hosts also receive plain string messages:

  • consents-loaded
  • consents-success
  • consents-error

These events exist for compatibility with older host integrations. They should not be the only contract for new consumers.

Inbound messages

The component currently accepts one host message:

  • SET_CONFIG

SET_CONFIG is required only when the URL contains enableCustomConfig=true. Without that flag, the component renders immediately with the default theme and ignores inbound SET_CONFIG.

The message can be sent as an object in browser iframe integrations or as JSON text in native bridge integrations:

json

{
  "type": "SET_CONFIG",
  "payload": {
    "theme": "light"
  }
}
ts

const iframe = document.querySelector<HTMLIFrameElement>('#mas-consents')

window.addEventListener('message', (event) => {
  const data = event.data

  if (typeof data === 'string') {
    if (data === 'consents-loaded') {
      // Compatibility ready signal
    }
    if (data === 'consents-success') {
      // Compatibility completion signal
    }
    if (data === 'consents-error') {
      // Compatibility error signal
    }
    return
  }

  if (data?.source !== 'consents') return

  switch (data.type) {
    case 'CONSENTS_CONFIG_REQUIRED':
      iframe?.contentWindow?.postMessage(
        {
          type: 'SET_CONFIG',
          payload: {
            theme: 'light',
          },
        },
        '*',
      )
      break
    case 'CONSENTS_LOADED':
      // Ready signal
      break
    case 'CONSENTS_COMPLETED':
      // Completion signal
      break
    case 'CONSENTS_ERROR':
      // Backend error payload is available in data.payload
      break
  }
})

Host applications should still validate event.origin according to their own security model.

HTTP calls made by the component

The host does not call the consents backend directly for the embedded flow. The component makes these calls using the URL context.

GET consents

text

GET /v2/orgs/{brand}/customers/{customerId}/consents

Forwarded headers:

  • Authorization: Bearer <token> when token exists
  • Accept-Language from normalized locale
  • x-sector from the URL
  • x-key if it was previously returned by an earlier response

Forwarded query params:

  • repeated view
  • optional choice
  • optional consent_ids when CustomConfig.consentIds is provided
  • optional display_format=SHORT on mobile xs

POST consents

text

POST /v2/orgs/{brand}/customers/{customerId}/consents

Forwarded headers:

  • Authorization: Bearer <token> when token exists
  • Accept-Language
  • x-sector
  • x-key captured from the previous GET response

The POST body includes actor_id, sell_channel, trace_id, customer selections, IP and capture date.

Runtime platform config

The frontend also loads /config.json at startup. This file is provided by the deployment and is not normally supplied by the consumer host.

Supported fields:

  • ENV
  • API_GATEWAY
  • FARO_COLLECTOR_URL
  • IS_OBSERVABILITY_ENABLED
  • TRACING_WHITELIST_URLS

If /config.json cannot be loaded and the runtime stays in LOCAL, the default local config remains active.

Examples

The examples below use enableCustomConfig=true so they show the complete handshake. If the host does not need to customize the component, remove enableCustomConfig=true from the URL and do not send SET_CONFIG.

Browser iframe

html

<iframe
  id="mas-consents"
  src="https://consents.masstack.com/?token=JWT&customerId=123&brand=masmovil&sector=telco&view=MANDATORY&lang=es&actorId=agent-123&sellChannel=ecare&traceId=trace-123&enableCustomConfig=true"
  style="width: 100%; height: 900px; border: 0;"
></iframe>

<script>
  const iframe = document.getElementById('mas-consents')

  window.addEventListener('message', (event) => {
    const data = event.data

    if (typeof data === 'string') {
      if (data === 'consents-success') {
        window.location.href = '/next-step'
      }
      if (data === 'consents-error') {
        console.error('Consents failed')
      }
      return
    }

    if (data?.source !== 'consents') return

    if (data.type === 'CONSENTS_CONFIG_REQUIRED') {
      iframe.contentWindow?.postMessage(
        {
          type: 'SET_CONFIG',
          payload: {
            theme: 'light',
            uiOptions: {
              groupDisplay: 'flat',
              collapsible: true,
            },
          },
        },
        '*',
      )
    }

    if (data.type === 'CONSENTS_COMPLETED') {
      window.location.href = '/next-step'
    }

    if (data.type === 'CONSENTS_ERROR') {
      console.error(data.payload)
    }
  })
</script>

Browser opener / popup

ts

const consentsUrl = new URL('https://consents.masstack.com/')

consentsUrl.search = new URLSearchParams({
  token: 'JWT',
  customerId: '123',
  brand: 'masmovil',
  sector: 'telco',
  view: 'MANDATORY',
  lang: 'es',
  actorId: 'agent-123',
  sellChannel: 'ecare',
  traceId: 'trace-123',
  enableCustomConfig: 'true',
  hostCommunication: 'opener',
  hostOrigin: window.location.origin,
}).toString()

const popup = window.open(consentsUrl.toString(), 'mas-consents', 'width=480,height=900')

window.addEventListener('message', (event) => {
  if (event.origin !== 'https://consents.masstack.com') return

  const data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data
  if (data?.source !== 'consents') return

  if (data.type === 'CONSENTS_CONFIG_REQUIRED') {
    popup?.postMessage(
      JSON.stringify({
        type: 'SET_CONFIG',
        payload: {
          theme: 'light',
        },
      }),
      'https://consents.masstack.com',
    )
  }

  if (data.type === 'CONSENTS_COMPLETED') {
    popup?.close()
  }
})

React Native WebView

tsx

import { useRef } from 'react'
import { WebView } from 'react-native-webview'

const consentsUrl =
  'https://consents.masstack.com/?token=JWT&customerId=123&brand=masmovil&sector=telco&view=MANDATORY&lang=es&actorId=agent-123&sellChannel=app&traceId=trace-123&enableCustomConfig=true'

export function ConsentsScreen() {
  const webViewRef = useRef<WebView>(null)

  const sendConfig = () => {
    const message = JSON.stringify({
      type: 'SET_CONFIG',
      payload: {
        theme: 'light',
        uiOptions: {
          groupDisplay: 'flat',
        },
      },
    })

    webViewRef.current?.injectJavaScript(`
      window.dispatchEvent(new MessageEvent('message', { data: ${JSON.stringify(message)} }));
      true;
    `)
  }

  return (
    <WebView
      ref={webViewRef}
      source={{ uri: consentsUrl }}
      onMessage={(event) => {
        const data = JSON.parse(event.nativeEvent.data)

        if (data.type === 'CONSENTS_CONFIG_REQUIRED') {
          sendConfig()
        }

        if (data.type === 'CONSENTS_COMPLETED') {
          // Continue host flow
        }

        if (data.type === 'CONSENTS_ERROR') {
          // Handle data.payload
        }
      }}
    />
  )
}

iOS WKWebView

swift

import UIKit
import WebKit

final class ConsentsViewController: UIViewController, WKScriptMessageHandler {
  private lazy var webView: WKWebView = {
    let contentController = WKUserContentController()
    contentController.add(self, name: "consentsBridge")

    let config = WKWebViewConfiguration()
    config.userContentController = contentController
    return WKWebView(frame: .zero, configuration: config)
  }()

  override func viewDidLoad() {
    super.viewDidLoad()
    view = webView

    var components = URLComponents(string: "https://consents.masstack.com/")!
    components.queryItems = [
      URLQueryItem(name: "token", value: "JWT"),
      URLQueryItem(name: "customerId", value: "123"),
      URLQueryItem(name: "brand", value: "masmovil"),
      URLQueryItem(name: "sector", value: "telco"),
      URLQueryItem(name: "view", value: "MANDATORY"),
      URLQueryItem(name: "lang", value: "es"),
      URLQueryItem(name: "actorId", value: "agent-123"),
      URLQueryItem(name: "sellChannel", value: "ios"),
      URLQueryItem(name: "traceId", value: "trace-123"),
      URLQueryItem(name: "enableCustomConfig", value: "true")
    ]

    webView.load(URLRequest(url: components.url!))
  }

  func userContentController(
    _ userContentController: WKUserContentController,
    didReceive message: WKScriptMessage
  ) {
    guard let raw = message.body as? String,
          let data = raw.data(using: .utf8),
          let payload = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
          payload["source"] as? String == "consents"
    else { return }

    if payload["type"] as? String == "CONSENTS_CONFIG_REQUIRED" {
      let config = #"{"type":"SET_CONFIG","payload":{"theme":"light"}}"#
      webView.evaluateJavaScript(
        "window.dispatchEvent(new MessageEvent('message', { data: '\(config)' }));"
      )
    }
  }
}

Android WebView

kotlin

class ConsentsActivity : AppCompatActivity() {
  private lateinit var webView: WebView

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    webView = WebView(this)
    setContentView(webView)

    webView.settings.javaScriptEnabled = true
    webView.addJavascriptInterface(ConsentsBridge(), "NativeBridge")

    val url = Uri.parse("https://consents.masstack.com/").buildUpon()
      .appendQueryParameter("token", "JWT")
      .appendQueryParameter("customerId", "123")
      .appendQueryParameter("brand", "masmovil")
      .appendQueryParameter("sector", "telco")
      .appendQueryParameter("view", "MANDATORY")
      .appendQueryParameter("lang", "es")
      .appendQueryParameter("actorId", "agent-123")
      .appendQueryParameter("sellChannel", "android")
      .appendQueryParameter("traceId", "trace-123")
      .appendQueryParameter("enableCustomConfig", "true")
      .build()

    webView.loadUrl(url.toString())
  }

  inner class ConsentsBridge {
    @JavascriptInterface
    fun postMessage(raw: String) {
      val message = JSONObject(raw)
      if (message.optString("source") != "consents") return

      if (message.optString("type") == "CONSENTS_CONFIG_REQUIRED") {
        val config = JSONObject()
          .put("type", "SET_CONFIG")
          .put("payload", JSONObject().put("theme", "light"))
          .toString()

        runOnUiThread {
          webView.evaluateJavascript(
            "window.dispatchEvent(new MessageEvent('message', { data: ${JSONObject.quote(config)} }));",
            null,
          )
        }
      }
    }
  }
}