Qamp Integration Guide

Add Qamp to your app in a few steps. Each step has a Copy agent prompt button for AI-assisted integration. For deeper coverage (idempotency, token refresh, local testing, pitfalls) see the full tenant guide.

1

Environment variables

Add to your .env file. QAMP_API_KEY is a secret — never commit it.

QAMP_URL=https://campaign-manager.up.railway.app
QAMP_API_KEY=<your-api-key>
QAMP_TENANT_ID=<TENANT_ID>
QAMP_SITE_ID=<SITE_ID>
2

Embed the SDK

Paste both tags near the top of <head> (or just before </body>). The small inline stub installs window.q synchronously so any inline bundle / IIFE / non-deferred script can call q.track(...) without optional chaining. The full SDK loads deferred and drains the stub's buffered calls on init.

<!-- Synchronous stub — installs window.q immediately, safe for parse-time calls -->
<script>!function(){var q=window.q=window.q||{};q.__q=q.__q||[];["track","identify","dismiss","reward","popup","on","off","resolve"].forEach(function(m){q[m]=q[m]||function(){q.__q.push([m,[].slice.call(arguments)])}})}();</script>

<!-- Full SDK, deferred -->
<script defer src="https://campaign-manager.up.railway.app/sdk/q.js" data-site="<SITE_ID>"></script>

<!-- For multi-environment setups, inject QAMP_URL/QAMP_SITE_ID at build time. -->
Verify: Open your site, then check the Qamp dashboard — a pageview should appear within seconds. If you skip the stub, any inline script that runs during HTML parsing (before the deferred tag executes) must guard its calls with window.q?..
3

Fire events from backend

Create a helper function to send lifecycle events to Qamp and get rewards back. Uses all four env vars from step 1.

const QAMP_TENANT_ID = parseInt(process.env.QAMP_TENANT_ID, 10);

const EMPTY = { rewards: [], totalCredits: 0, popups: [], sideEffects: [], errors: [], warnings: [] };

async function fireQampEvent(type, userId, context = {}) {
  if (!process.env.QAMP_API_KEY || !process.env.QAMP_URL) return EMPTY;
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 3000);
  try {
    const res = await fetch(`${process.env.QAMP_URL}/api/events`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${process.env.QAMP_API_KEY}`
      },
      body: JSON.stringify({ type, userId, tenantId: QAMP_TENANT_ID, context }),
      signal: controller.signal
    });
    if (!res.ok) { console.error(`Qamp: ${res.status}`); return { ...EMPTY, errors: [`HTTP ${res.status}`] }; }
    return await res.json();
  } catch (err) { console.error('Qamp:', err.message); return { ...EMPTY, errors: [err.message] }; }
  finally { clearTimeout(timeout); }
}
4

Handle user signup

Fire user_signup when a new user registers. Credit their account and show the reward popup.

const result = await fireQampEvent('user_signup', userId, {
  isNew: true,                 // required — always pass literal true, not a variable
  referrerId: referrerUserId   // optional — triggers referral rewards for the referrer
});

// Credit the user
if (result.totalCredits > 0) await creditUser(userId, result.totalCredits);

// Persist any side effects (e.g. set_referred_by)
for (const fx of result.sideEffects) {
  if (fx.action === 'set_referred_by') await setUserReferrer(fx.userId, fx.referrerId);
}

// Show reward popup — push to client via your websocket, SSE, or HTTP response
for (const popup of result.popups) {
  sendToClient(userId, { type: 'qamp:reward', popup });
  // sendToClient = your app's push mechanism, e.g.:
  // ws.send(JSON.stringify(...))  or  res.json({ reward: popup })
}
Verify: Fire a test signup — if a matching campaign is active, result.totalCredits will be > 0.
5

Other events

Same pattern, same response handling. Handle totalCredits, popups, and sideEffects identically.

// Game complete — result is required ('win' or 'loss')
await fireQampEvent('game_complete', userId, {
  result: 'win',                // required — 'win' or 'loss'
  referrerId: referrerUserId,   // optional — triggers referral_game_complete
  isFirstCompletedGame: true,   // optional — required for referral_game_complete to fire
  gameResultId: 'gr_abc123',    // optional — passed through for logging
  displayName: 'Chess Match'    // optional — shown in admin dashboard
});

// Share — no required context
await fireQampEvent('share', userId, {});
6

Frontend SDK helpers

With the stub + defer install from step 2, window.q is defined from the first byte after the stub tag and buffered calls drain on init — no optional chaining needed anywhere. (If you installed only the single defer tag, parse-time inline scripts still need q?. guards.) Call identify() on login and signup to link the anonymous session.

// Link session to logged-in user — call on login AND after signup
q.identify(userId, { name: 'Jane', plan: 'pro' });

// Show a reward popup returned from fireQampEvent
q.reward(popup);  // popup object from result.popups

// Track a custom frontend event (or use [data-q-track] — see below)
q.track('button_click', { label: 'signup-cta' });

// Dismiss a specific campaign
q.dismiss('spring-sale');
7

Declarative event tracking

Annotate DOM elements with data-q-track to fire events on click without imperative JS. The delegated listener covers dynamically-injected elements automatically.

<button data-q-track="signup_clicked">Sign up</button>
<button data-q-track="level_started" data-q-props='{"level":3}'>Play</button>
<a href="/share" data-q-track="share_clicked">Share</a>
Verify: Click one of these buttons and check the dashboard events feed — the named event should appear within seconds. Because the listener re-reads the attribute on every click, mutating data-q-props dynamically works as expected.
8

Server-driven popups & named CTA actions

Fire popups whose copy, theme, and frequency are managed in the Qamp portal — not hardcoded in your app. Use q.on() to wire named CTA handlers once, and admins reference them by name from the display rule.

// Register named CTA action handlers once (near app boot)
q.on('invite_friend', () => openNativeShareSheet());
q.on('claim_preoffer', () => location.href = '/store?promo=1');

// Fire a popup by its admin-defined slug — copy comes from the portal,
// not from your code. Edit title/body/theme in Qamp and it updates on next load.
inviteBtn.addEventListener('click', () => {
  q.popup({ slug: 'invite_friends' });
});

// CTA click resolution order:
//   1. per-call onCta callback (if passed)   — wins if present
//   2. q.on() handler for content.cta_action — wins if registered
//   3. content.cta_url navigation            — fallback
// Rules can safely define both cta_action and cta_url.
Verify: Admin edits copy in the portal next page load reflects it. Popup content is cached in memory for the page lifetime (not localStorage). To bust during QA: hard-reload the page; the server also sets a 30-second Cache-Control so allow ~30s between an edit and a fresh-session test.

Full tenant guide

Deeper reference: idempotency patterns, client-direct events via q.resolve(), JWT token refresh strategies, local testing / staging setup, frontend test stubs, and common pitfalls.

// Full guide with worked examples, rendered from the repo:
// ${QAMP_URL}/docs/tenant-guide

Read-only API endpoints

Two additional service-to-service endpoints for reading campaign and participation data. Same API key auth as fireQampEvent.

// Get all active promotions (for rendering in your UI)
const promos = await fetch(`${process.env.QAMP_URL}/api/promotions/active/full`, {
  headers: { 'Authorization': `Bearer ${process.env.QAMP_API_KEY}`, 'X-Tenant-ID': String(QAMP_TENANT_ID) }
}).then(r => r.json());
// promos.promotions = [{ slug, title, trigger_type, reward_amount, ... }]

// Get a user's participation stats (progress, rewards granted)
const parts = await fetch(`${process.env.QAMP_URL}/api/participations/${userId}`, {
  headers: { 'Authorization': `Bearer ${process.env.QAMP_API_KEY}`, 'X-Tenant-ID': String(QAMP_TENANT_ID) }
}).then(r => r.json());
// parts.participations = [{ slug, progress, reward_granted, max_reward_per_user, status }]

Authentication

ContextHeader
Server-to-serverAuthorization: Bearer <CAMPAIGN_API_KEY>
MCP / AI agentAuthorization: Bearer cm_live_<key>

Event Response

FieldDescription
totalCreditsSum of credit rewards. Credit the user’s account with this.
popupsPass each to window.q.reward() on the frontend.
rewardsPer-campaign breakdown: slug, amount, type, title.
errorsFatal errors during evaluation.
warningsNon-fatal issues (e.g., isNew not true).

Error Codes

StatusMeaning
400Invalid type, userId, or tenant mismatch
401Missing or invalid Authorization header
429Rate limited (30 req/min per IP)

Rate Limits

EndpointLimit
POST /api/events30 req / 60s per IP
POST /api/collectNo limit (batch up to 50)
/mcp30 req / 60s per IP