D Diagent docs

API reference

Signed CTA context

A CTA configured with forward context tacks a signed payload onto its outbound URL so the destination site can recognise the visitor without re-asking who they are. The payload is encoded as three query-string parameters and signed with an HMAC-SHA256 secret that lives on the workspace.

Where to enable it
Open an agent โ†’ Behavior rules & CTAs โ†’ pick or create a link_with_context CTA. Toggle Forward context and select which fields the destination site should receive.

Outbound URL shape

When forwarding is on, Pitchbar appends three parameters to the base URL you configured on the CTA:

{base-url}?pitchbar_ctx=<base64url-json>
         &pitchbar_ts=<unix-seconds>
         &pitchbar_sig=<hex-hmac-sha256>
  • pitchbar_ctx โ€” base64url-encoded JSON of the whitelisted fields you chose to forward.
  • pitchbar_ts โ€” unix timestamp at which the URL was minted; used for replay protection.
  • pitchbar_sig โ€” hex HMAC-SHA256 of "{pitchbar_ctx}.{pitchbar_ts}" using the workspace's cta_context_secret.

A CTA with no forward-fields selected emits the URL verbatim โ€” the extra params only appear once the operator opts in.

Whitelisted fields

Pitchbar only ships fields the operator explicitly enabled per CTA, chosen from this whitelist:

FieldSource
conversation_idActive conversation row
agent_idOwning agent
page_urlPage the visitor was on when chat opened
visitor_emailLatest captured Lead.email
visitor_nameLatest captured Lead.name
captured_fieldsLead.fields JSON minus password/token/secret/api_key keys

Sensitive-looking custom field keys (anything containing password, pwd, secret, token, api_key, apikey, or private) are stripped from captured_fields before signing โ€” defence-in-depth.

Find your workspace secret

The signing secret lives on the workspace row as cta_context_secret. Pitchbar mints one automatically the first time a CTA emits a signed URL. You can copy it from Settings โ†’ API tokens (the Signed CTA context card) and rotate it when you need to revoke trust on receiving sites.

Treat this secret like a webhook key
Never ship the secret to the browser. Verification happens on the receiving site's server โ€” the value should sit in an environment variable on that server, not in client-side JavaScript.

Replay window

Pitchbar enforces a 5-minute replay window (300s) by rejecting verifications whose pitchbar_ts is more than that far from the current time. The verifier on your side must apply the same check โ€” without it an attacker who scrapes the URL out of a referrer log can replay it forever.

Verifying the signature

The pattern is the same in every language: rebuild the HMAC over the exact ctx.ts string with your workspace secret, compare against the inbound pitchbar_sig using a constant-time check, and only then decode pitchbar_ctx to read the payload.

PHP

$ctx = (string) ($_GET['pitchbar_ctx'] ?? '');
$ts  = (string) ($_GET['pitchbar_ts'] ?? '');
$sig = (string) ($_GET['pitchbar_sig'] ?? '');
$secret = getenv('PITCHBAR_CTA_SECRET');

if ($ctx === '' || $ts === '' || $sig === '' || $secret === '') {
    http_response_code(401);
    exit;
}

// Replay window โ€” 5 minutes either direction.
if (abs(time() - (int) $ts) > 300) {
    http_response_code(401);
    exit;
}

$expected = hash_hmac('sha256', $ctx . '.' . $ts, $secret);
if (! hash_equals($expected, $sig)) {
    http_response_code(401);
    exit;
}

// Signature valid โ€” decode the payload. Note base64url, not standard base64.
$padded = $ctx . str_repeat('=', (4 - strlen($ctx) % 4) % 4);
$json   = base64_decode(strtr($padded, '-_', '+/'), true);
$payload = $json === false ? null : json_decode($json, true);
// $payload now contains the whitelisted fields the operator forwarded.

Node.js

const crypto = require('crypto');

function verifyCtaContext(query, secret) {
    const ctx = String(query.pitchbar_ctx || '');
    const ts  = String(query.pitchbar_ts  || '');
    const sig = String(query.pitchbar_sig || '');

    if (!ctx || !ts || !sig) {
        return null;
    }

    // Replay window.
    if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
        return null;
    }

    const expected = crypto
        .createHmac('sha256', secret)
        .update(`${ctx}.${ts}`)
        .digest('hex');

    if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) {
        return null;
    }

    const padded = ctx + '='.repeat((4 - (ctx.length % 4)) % 4);
    const json = Buffer
        .from(padded.replace(/-/g, '+').replace(/_/g, '/'), 'base64')
        .toString('utf8');

    try {
        return JSON.parse(json);
    } catch {
        return null;
    }
}

Python

import base64, hmac, hashlib, json, time

def verify_cta_context(query, secret: str):
    ctx = query.get('pitchbar_ctx', '')
    ts  = query.get('pitchbar_ts',  '')
    sig = query.get('pitchbar_sig', '')

    if not ctx or not ts or not sig:
        return None

    try:
        if abs(time.time() - int(ts)) > 300:
            return None
    except ValueError:
        return None

    expected = hmac.new(
        secret.encode(),
        f"{ctx}.{ts}".encode(),
        hashlib.sha256,
    ).hexdigest()

    if not hmac.compare_digest(expected, sig):
        return None

    padded = ctx + '=' * ((4 - len(ctx) % 4) % 4)
    raw = base64.urlsafe_b64decode(padded.encode())
    try:
        return json.loads(raw)
    except json.JSONDecodeError:
        return None

Example payload

A CTA configured to forward conversation_id, visitor_email, and page_url ships:

{
    "conversation_id": "01JRX4D8YQ8KEXP3F5VZ8MEXAM",
    "visitor_email": "[email protected]",
    "page_url": "https://customer-site.com/pricing"
}

base64url-encoded into pitchbar_ctx, paired with the current pitchbar_ts, and signed with your secret.

Why not the outbound webhook?

The webhook subscription delivers the full conversation transcript asynchronously โ€” perfect for analytics or CRM push, but useless when a visitor clicks a CTA and you want to land them on a personalised page before any background job finishes. The signed CTA context exists for that fast path: it travels with the click and arrives with the request.

Keep using webhooks for anything that needs the full body, or for leads that you want to fan out to multiple downstream systems. They're complementary, not redundant.