API reference
Signed CTA context
Signed CTA context lets you forward visitor data to your CTA destinations through cryptographically signed URL parameters. When a CTA is configured with forward context, three query parameters are appended to the destination URL — and your backend can verify they really came from Glozr.
URL structure
The outbound URL follows this pattern:
{base-url}?pitchbar_ctx=<base64url-json>
&pitchbar_ts=<unix-seconds>
&pitchbar_sig=<hex-hmac-sha256>
Each parameter serves a specific purpose: pitchbar_ctx carries the visitor payload, pitchbar_ts enables replay protection, and pitchbar_sig proves the URL was minted by Glozr.
Whitelisted fields
You can selectively forward any of the following:
conversation_id— the active conversation.agent_id— the owning agent.page_url— the visitor's current page.visitor_email— the latest captured email.visitor_name— the latest captured name.captured_fields— custom lead fields, with sensitive keys automatically stripped.
Security measures
- HMAC-SHA256 signature using a workspace-scoped secret known only to Glozr and your server.
- Automatic filtering of sensitive field names — any field name matching
password,token,secretorapi_keyis stripped before signing. - Five-minute replay window — URLs older than 300 seconds are rejected on verification.
- Constant-time comparison — both sides use timing-safe equality to resist signature-recovery attacks.
Note. The workspace secret must stay server-side. Never expose it in browser code, mobile apps or any client environment a user can inspect.
Verification
PHP:
function verify_glozr_ctx(array $query, string $secret): ?array {
$ctx = $query['pitchbar_ctx'] ?? null;
$ts = (int)($query['pitchbar_ts'] ?? 0);
$sig = $query['pitchbar_sig'] ?? '';
if (!$ctx || !$ts || !$sig) return null;
if (abs(time() - $ts) > 300) return null;
$expected = hash_hmac('sha256', "{$ts}.{$ctx}", $secret);
if (!hash_equals($expected, $sig)) return null;
return json_decode(base64_decode(strtr($ctx, '-_', '+/')), true);
}
Node.js:
const crypto = require('crypto');
function verify(query, secret) {
const { pitchbar_ctx: ctx, pitchbar_ts: ts, pitchbar_sig: sig } = query;
if (!ctx || !ts || !sig) return null;
if (Math.abs(Date.now()/1000 - Number(ts)) > 300) return null;
const expected = crypto.createHmac('sha256', secret)
.update(`${ts}.${ctx}`).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected,'hex'), Buffer.from(sig,'hex'))) return null;
return JSON.parse(Buffer.from(ctx, 'base64url').toString());
}
Python:
import base64, hmac, hashlib, json, time
def verify(query, secret):
ctx = query.get('pitchbar_ctx')
ts = int(query.get('pitchbar_ts') or 0)
sig = query.get('pitchbar_sig', '')
if not (ctx and ts and sig):
return None
if abs(time.time() - ts) > 300:
return None
expected = hmac.new(secret.encode(),
f"{ts}.{ctx}".encode(),
hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, sig):
return None
padded = ctx + '=' * (-len(ctx) % 4)
return json.loads(base64.urlsafe_b64decode(padded))
Stacked CTA cards
An agent can carry several CTA rules, each with its own priority integer. When more than one matches the same reply, the widget renders them together as a stacked list of cards — for example Pricing, Demo and Docs side by side — rather than just the single top match. CTAs are evaluated in descending priority order against the page URL, conversation language and the assistant's text, and the matches are returned in that order so the highest-priority card sits first.
A hard cap of three cards per reply applies (MAX_CTAS = 3): once three matching CTAs are collected, evaluation stops. The streamed done event carries the full set as ctas; a singular cta field is still populated with the first match so older widget bundles and JSON-fallback clients always render at least one card. Signed-context forwarding is applied per card, so each CTA can opt into its own whitelisted payload independently.