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
Embedded component authentication
Token expected by the component
The token URL parameter is the token that mas-consents forwards to the Consents backend as Authorization: Bearer <token>.
For customer-facing integrations, the host should not put a broad channel, agent, or customer token directly in the URL.
The URL can be visible for a short time in the browser, WebView, history, traces, or infrastructure logs before the component clears the query string.
Getting a securized token
For public channels, the component should receive a token scoped to the embedded consent flow. That token is obtained through the Consents securization endpoint, available in the Consents API reference:
POST /v2/orgs/{brand}/securizeCall this endpoint with credentials authorized to securize the payload for the embedded frontend, using the consents-front-operator role.
Those credentials must not be exposed in public client code.
The body must include the context Consents needs to scope authorization:
{
"actor_id": "<actor_id>",
"customer_id": "<customer_id>"
}The response contains the token expected by the embedded frontend:
{
"web_component_token": "<token_for_mas_consents>"
}Using the token in the URL
Pass that value in the URL as token=<token_for_mas_consents>.
mas-consents uses it for its Consents GET and POST calls.
The token is still sensitive, even though it is scoped to the Consents domain and authorized context. The host should avoid writing the full URL to logs, errors, analytics, or support tooling.
URL parameters
The frontend reads the query string once during startup. For real backend calls, the host must provide authentication, customer context, and tracing data.
Authentication
| Parameter | Runtime behavior | Consumer expectation |
|---|---|---|
token | Forwarded as Authorization: Bearer <token> when present | Required for real backend calls |
For public channels, use the web_component_token returned by securization.
Customer context
| Parameter | Runtime behavior | Consumer expectation |
|---|---|---|
customerId | Used in GET/POST paths | Required for real backend calls |
brand | Used in GET/POST paths. mock enables local mock behavior | Required for real backend calls |
sector | Forwarded as x-sector header | Required for real backend calls |
Flow selection
| Parameter | Runtime behavior | Consumer expectation |
|---|---|---|
view | Repeated params are preserved and sent as repeated query params | Required by most host flows |
choice | Optional filter passed to the backend | Optional |
Localization
| Parameter | Runtime behavior | Consumer expectation |
|---|---|---|
lang | Normalized against the supported locale list. Falls back to es when missing or invalid | Recommended |
locales | Optional comma-separated list. Defaults to all supported locales | Optional |
Submit and tracing
| Parameter | Runtime behavior | Consumer expectation |
|---|---|---|
actorId | Included in POST body as actor_id | Expected for real submit flows |
sellChannel | Included in POST body as sell_channel | Expected for real submit flows |
traceId | Included in POST body as trace_id | Expected for tracing and submit flows |
Communication and customization
| Parameter | Runtime behavior | Consumer expectation |
|---|---|---|
enableCustomConfig | Only exact value true has an effect. It makes the component wait for host JSON configuration | Optional |
hostCommunication | opener enables the browser opener communication mode | Optional |
hostOrigin | Required with hostCommunication=opener; invalid origins are ignored | Optional unless opener mode is used |
Base URL example without customization:
https://consents.masstack.com/?token=<web_component_token>&customerId=<customer_id>&brand=<brand>§or=<sector>&view=MANDATORY&lang=es&actorId=<actor_id>&sellChannel=<channel>&traceId=<trace_id>Build the URL with URLSearchParams or an equivalent API so the token is encoded correctly and the host does not concatenate query strings by hand.
Supported locales:
eseucaengl
Supported view values:
MANDATORYPORTFOLIOSGDAINTERNALPRIVATEWHATSAPPOGW_DEVICE_SWAPOGW_SIM-SWAPOGW_NUMBER-VERIFICATIONOGW_KYC-MATCH
Multiple views are passed as repeated params:
?view=MANDATORY&view=PORTFOLIOSupported choice values:
PENDINGREJECTEDACCEPTED
Common flows
The view value depends on the business case the host needs to run.
As a reference:
- for a signup or order flow, the host usually requests mandatory consents with
view=MANDATORY - for a consent management or modification screen, the host should use the view agreed for that channel, such as a portfolio view
- if the host needs to restrict the load to consents in a specific state, it can combine the view with
choice
When the backend must receive more than one view, repeat the view parameter in the URL instead of sending a comma-separated string.
Query string cleanup
The component reads the URL params during bootstrap and then clears the query string from the browser URL.
The startup context is kept for the current browser session so the component can recover after a page reload.
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:
{
"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 whenenableCustomConfig=trueCONSENTS_LOADED: emitted after consents are loadedCONSENTS_COMPLETED: emitted after a successful submit or when the backend says the campaign is already completeCONSENTS_ERROR: emitted with backend error payload when loading or submitting fails
Compatibility string events
Browser iframe hosts also receive plain string messages:
consents-loadedconsents-successconsents-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:
{
"type": "SET_CONFIG",
"payload": {
"theme": "light"
}
}Recommended browser listener
const iframe = document.querySelector<HTMLIFrameElement>('#mas-consents')
const consentsOrigin = new URL(iframe?.src ?? 'https://consents.masstack.com').origin
window.addEventListener('message', (event) => {
if (event.origin !== consentsOrigin) return
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
GET /v2/orgs/{brand}/customers/{customerId}/consentsForwarded headers:
Authorization: Bearer <token>whentokenexistsAccept-Languagefrom normalized localex-sectorfrom the URLx-keyif it was previously returned by an earlier response
Forwarded query params:
- repeated
view - optional
choice - optional
consent_idswhenCustomConfig.consentIdsis provided - optional
display_format=SHORTon mobilexs
POST consents
POST /v2/orgs/{brand}/customers/{customerId}/consentsForwarded headers:
Authorization: Bearer <token>whentokenexistsAccept-Languagex-sectorx-keycaptured 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:
ENVAPI_GATEWAYFARO_COLLECTOR_URLIS_OBSERVABILITY_ENABLEDTRACING_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
<iframe
id="mas-consents"
src="https://consents.masstack.com/?token=JWT&customerId=123&brand=masmovil§or=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')
const consentsOrigin = new URL(iframe.src).origin
window.addEventListener('message', (event) => {
if (event.origin !== consentsOrigin) return
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
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
import { useRef } from 'react'
import { WebView } from 'react-native-webview'
const consentsUrl =
'https://consents.masstack.com/?token=JWT&customerId=123&brand=masmovil§or=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
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
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,
)
}
}
}
}
}