BricqsBricqs
Documentation

Headless SDK

Enterprise-grade React hooks that return pure data, state, and action handlers — zero rendering. Build your own UI with your design system while Bricqs handles all backend logic.

Architecture

The Headless SDK is a layer of React hooks that sit on top of the existing SDK infrastructure. Hooks call the Bricqs API through the SDK client, manage state with React, and return typed objects — you render everything yourself.

┌──────────────────────────────────────────────────────┐
│  Your Custom UI (your components, your design system) │
│                                                       │
│  useQuiz()    useSpinWheel()    useForm()             │
│  usePoll()    useSurvey()       useBadges()           │
│  usePoints()  useTier()         useChallenge()        │
│  useLeaderboard()               useRewards()          │
│                                                       │
│  Returns: data + state + handlers. Zero JSX.          │
├───────────────────────────────────────────────────────┤
│  BricqsProvider (context + SDK client)                │
│                                                       │
│  BricqsClient.activities — validate, complete         │
│  BricqsClient.challenges — enroll, progress           │
│  BricqsClient.leaderboards — rankings                 │
│  BricqsClient.rewards — claimed rewards               │
│  BricqsClient.badges — earned/unearned status         │
│  BricqsClient.points — balance, transactions          │
├───────────────────────────────────────────────────────┤
│  Bricqs API Server                                    │
│                                                       │
│  Activity validation + action execution               │
│  Points, badges, tiers, rewards (atomic, server-side) │
│  Challenge evaluation + progress tracking             │
└───────────────────────────────────────────────────────┘

Quick Start

Build a completely custom quiz UI in 5 minutes. Bricqs handles scoring, point awards, badge unlocks, and reward claims — you handle the rendering.

import {
  BricqsProvider,
  useEngagementData,
  useQuiz,
  usePoints,
} from '@bricqs/sdk-react';

// 1. Wrap your app
function App() {
  return (
    <BricqsProvider config={{ apiKey: 'bq_live_YOUR_KEY' }}>
      <MyCustomQuiz />
    </BricqsProvider>
  );
}

// 2. Load engagement and get quiz config
function MyCustomQuiz() {
  const { getQuizConfig, getComponent, isLoading } = useEngagementData({
    engagementId: 'YOUR_ENGAGEMENT_UUID',
  });

  const quizConfig = getQuizConfig();
  const quizComponent = getComponent('quiz');
  const points = usePoints({ engagementId: 'YOUR_ENGAGEMENT_UUID' });

  if (isLoading || !quizConfig || !quizComponent) {
    return <div>Loading...</div>;
  }

  return (
    <QuizUI
      engagementId="YOUR_ENGAGEMENT_UUID"
      activityId={quizComponent.id}
      config={quizConfig}
      pointsBalance={points.balance}
    />
  );
}

// 3. Build your own quiz UI — zero Bricqs styling
function QuizUI({ engagementId, activityId, config, pointsBalance }) {
  const quiz = useQuiz({ engagementId, activityId, config });

  if (quiz.isComplete) {
    return (
      <div className="my-results">
        <h2>{quiz.feedback?.title}</h2>
        <p>Score: {quiz.score?.percentage}%</p>
        {quiz.actionResults?.points_awarded > 0 && (
          <p className="points">+{quiz.actionResults.points_awarded} points!</p>
        )}
        <button onClick={quiz.reset}>Try Again</button>
      </div>
    );
  }

  return (
    <div className="my-quiz">
      <div className="progress-bar" style={{ width: quiz.progress + '%' }} />
      <p className="balance">{pointsBalance} pts</p>
      <h3>{quiz.currentQuestion.question}</h3>
      <div className="options">
        {quiz.currentQuestion.options.map((opt, i) => (
          <button
            key={i}
            className={quiz.selectedAnswer === i ? 'selected' : ''}
            onClick={() => quiz.selectAnswer(i)}
          >
            {opt}
          </button>
        ))}
      </div>
      <div className="nav">
        {!quiz.isFirstQuestion && <button onClick={quiz.previous}>Back</button>}
        {quiz.isLastQuestion ? (
          <button onClick={quiz.submit} disabled={quiz.isSubmitting}>
            Submit
          </button>
        ) : (
          <button onClick={quiz.next}>Next</button>
        )}
      </div>
    </div>
  );
}

useEngagementData

Loads an engagement and provides typed accessors for component configs. This is the entry point — load your engagement first, then pass configs to activity hooks.

const {
  engagement,     // Full engagement object
  components,     // All components in the engagement
  isLoading,      // True while loading
  error,          // Error object if load failed

  // Typed config accessors
  getComponent,       // (type: string) => Component | null
  getComponentConfig, // <T>(type: string) => T | null
  getQuizConfig,      // () => QuizConfig | null
  getSpinWheelConfig, // () => SpinWheelConfig | null
  getFormConfig,      // () => FormConfig | null
  getPollConfig,      // () => PollConfig | null
  getSurveyConfig,    // () => SurveyConfig | null
} = useEngagementData({
  engagementId: 'YOUR_UUID',
});

Activity Hooks

Each activity type has a dedicated hook that manages the full lifecycle: loading config, tracking user interactions, validating submissions, and executing server-side actions (points, badges, rewards).

useQuiz

Full quiz state — question navigation, answer tracking, client-side scoring, and server-side action execution.

const quiz = useQuiz({
  engagementId: 'YOUR_UUID',
  activityId: 'QUIZ_COMPONENT_ID',
  config: quizConfig,  // from useEngagementData().getQuizConfig()
});

// Navigation
quiz.currentQuestion    // { question: string, options: string[], ... }
quiz.currentIndex       // number (0-based)
quiz.totalQuestions     // number
quiz.isFirstQuestion    // boolean
quiz.isLastQuestion     // boolean
quiz.progress           // number (0-100)
quiz.next()             // Go to next question
quiz.previous()         // Go to previous question
quiz.goTo(index)        // Jump to specific question

// Answers
quiz.selectedAnswer     // number | null (selected option index)
quiz.answers            // (number | null)[] (all answers)
quiz.selectAnswer(i)    // Select an option

// Submission
quiz.submit()           // Submit answers (async — calls server)
quiz.isSubmitting       // boolean

// Results (after submit)
quiz.isComplete         // boolean
quiz.score              // { score, maxScore, percentage, passed }
quiz.feedback           // { title, message } (performance-based)
quiz.actionResults      // Server results: points, badges, rewards
quiz.reset()            // Reset for retry

useSpinWheel

Two-step spin flow: call spin() to get the result from the server, animate your wheel to the target angle, then call confirmResult() to record the completion.

const wheel = useSpinWheel({
  engagementId: 'YOUR_UUID',
  activityId: 'WHEEL_COMPONENT_ID',
  config: spinWheelConfig,
});

// State
wheel.segments          // SpinWheelSegment[] — segment labels, colors, weights
wheel.isSpinning        // boolean
wheel.result            // SpinWheelResult | null (after spin)
wheel.rotation          // number — target rotation degrees for CSS animation
wheel.actionResults     // Server results after confirmResult()

// Actions
await wheel.spin()      // Call server, get result + target rotation
await wheel.confirmResult() // After animation ends — record completion
wheel.reset()           // Reset for another spin
Animation tip: Use wheel.rotation with a CSS transform: rotate() transition. Call wheel.confirmResult() in the onTransitionEnd callback.

useForm

Form field management with validation, touched tracking, and submission.

const form = useForm({
  engagementId: 'YOUR_UUID',
  activityId: 'FORM_COMPONENT_ID',
  config: formConfig,
});

// Fields
form.fields             // FormField[] — field definitions (label, type, required, etc.)
form.values             // Record<string, unknown> — current values
form.errors             // Record<string, string> — validation errors
form.touched            // Record<string, boolean> — which fields have been touched

// State
form.isValid            // boolean — all fields pass validation
form.isSubmitting       // boolean
form.actionResults      // Server results after submit

// Actions
form.setValue(fieldId, value)  // Update a field value
form.setTouched(fieldId)      // Mark a field as touched (show validation)
form.validate()               // Run validation on all fields
form.submit()                 // Submit the form (async)
form.reset()                  // Reset all values
Validation: Built-in validators for email, phone, URL, and custom regex patterns. Validation runs automatically on touched fields and on submit.

usePoll

Single or multi-select poll with live results.

const poll = usePoll({
  engagementId: 'YOUR_UUID',
  activityId: 'POLL_COMPONENT_ID',
  config: pollConfig,
});

poll.question           // string
poll.options            // PollOption[] — { id, label }
poll.selectedOptions    // string[] — selected option IDs
poll.isMultiSelect      // boolean
poll.isSubmitted        // boolean
poll.results            // PollResults | null — vote counts per option
poll.actionResults      // Server results

poll.select(optionId)   // Toggle an option
poll.submit()           // Submit votes
poll.reset()            // Reset

useSurvey

Multi-question survey with navigation and progress tracking.

const survey = useSurvey({
  engagementId: 'YOUR_UUID',
  activityId: 'SURVEY_COMPONENT_ID',
  config: surveyConfig,
});

survey.questions         // SurveyQuestion[]
survey.currentIndex      // number
survey.totalQuestions    // number
survey.progress          // number (0-100)
survey.responses         // Record<string, unknown>
survey.isComplete        // boolean
survey.isSubmitting      // boolean
survey.actionResults     // Server results

survey.setResponse(questionId, value)
survey.next()
survey.previous()
survey.submit()
survey.reset()

Progression Hooks

Access gamification data — points, badges, tiers, leaderboards, rewards, and challenges. All hooks auto-refresh on relevant events and support manual refresh.

usePoints

const points = usePoints({ engagementId: 'YOUR_UUID' });

points.balance          // number — current balance
points.totalEarned      // number — lifetime earned
points.currentTier      // { tier_name, tier_level, color } | null
points.transactions     // PointsTransaction[] — recent history
points.isLoading        // boolean
points.refresh()        // Manual refresh

// Auto-refreshes when:
// - points:awarded event fires
// - tier:changed event fires

useTier

const tier = useTier({ engagementId: 'YOUR_UUID' });

tier.currentTier        // CurrentTier | null
tier.tierName           // string | null (e.g. "Gold")
tier.tierLevel          // number (e.g. 3)
tier.tierColor          // string | null (hex color)
tier.pointsToNext       // number | null (points needed for next tier)
tier.nextTierName       // string | null
tier.isLoading          // boolean
tier.refresh()          // Manual refresh

useBadges

const badges = useBadges({
  engagementId: 'YOUR_UUID',
  badgeCodes: ['first_quiz', 'streak_7', 'top_scorer'], // optional filter
  refreshInterval: 30000, // ms, default 30s
});

badges.badges           // BadgeStatusEntry[] — all badges
badges.earned           // BadgeStatusEntry[] — earned only
badges.unearned         // BadgeStatusEntry[] — not yet earned
badges.isLoading        // boolean
badges.refresh()        // Manual refresh
badges.isEarned('code') // Check if a specific badge is earned

// Each badge: { code, name, description, icon, rarity, earned, earnedAt }
// Auto-refreshes every 30s + instantly on badge:unlocked events

useChallenge

Full challenge lifecycle — load, enroll, track progress, and display leaderboard.

const challenge = useChallenge({
  engagementId: 'YOUR_UUID',
  autoEnroll: true,       // Auto-enroll on load (default: false)
  refreshInterval: 30000, // ms
});

// Challenge definition
challenge.challenge           // ChallengeDetail | null
challenge.objectives          // ChallengeObjective[]
challenge.milestones          // ChallengeMilestone[]

// Enrollment
challenge.isEnrolled          // boolean
challenge.enroll()            // Manual enroll action

// Progress
challenge.progress            // ChallengeProgress | null
challenge.progressPercentage  // number (0-100)
challenge.objectiveProgress   // ObjectiveProgress[]
challenge.completedObjectives // number
challenge.totalObjectives     // number

// Leaderboard
challenge.leaderboard         // LeaderboardEntry[]
challenge.myRank              // number | null

// State
challenge.isLoading           // boolean
challenge.error               // Error | null
challenge.refresh()           // Manual refresh

// Auto-refreshes on activity:completed events

useLeaderboard

Supports both progression leaderboards (by code) and challenge leaderboards.

// Progression leaderboard (by code)
const lb = useLeaderboard({
  code: 'main_leaderboard',
  engagementId: 'YOUR_UUID',
  limit: 20,
  refreshInterval: 30000,
});

// Or challenge leaderboard (by challengeId)
const lb = useLeaderboard({
  challengeId: 'CHALLENGE_UUID',
  engagementId: 'YOUR_UUID',
  limit: 10,
});

lb.entries              // LeaderboardEntry[] — { rank, name, score, userId, change }
lb.myRank               // number | null
lb.totalParticipants    // number
lb.isLoading            // boolean
lb.refresh()            // Manual refresh

useRewards

const rewards = useRewards({ engagementId: 'YOUR_UUID' });

rewards.rewards         // ClaimedReward[] — all claimed rewards
rewards.totalClaimed    // number
rewards.isLoading       // boolean
rewards.refresh()       // Manual refresh

// Each reward: { id, rewardName, rewardType, codeValue, value, claimedAt, expiresAt }
// Auto-refreshes instantly on reward:claimed events

Server-Side Actions

When a headless activity hook submits (e.g., quiz.submit()), the server executes all configured on-completion actions atomically. The actionResults object contains the outcomes.

// After quiz.submit() resolves:
const results = quiz.actionResults;

// Points
if (results?.points_awarded > 0) {
  showPointsAnimation(results.points_awarded);
}

// Badges
if (results?.badges_unlocked?.length > 0) {
  results.badges_unlocked.forEach(badge => {
    showBadgeUnlocked(badge.name, badge.icon);
  });
}

// Tier upgrade
if (results?.tier_upgrade) {
  showTierCelebration(results.tier_upgrade.new_tier);
}

// Reward claimed
if (results?.reward_claimed) {
  showCouponCode(results.reward_claimed.code_value);
}

// Challenge progress
if (results?.challenge_progress) {
  showProgressUpdate(results.challenge_progress);
}
Key principle: Actions are configured in the Builder (via on-completion actions) and executed atomically on the server. The headless SDK doesn't need to know about individual actions — it just submits the activity and receives the combined results.

Full Example: Custom Campaign Page

A complete example with a custom quiz UI, points display, badge shelf, and challenge progress — all with your own design system.

import {
  BricqsProvider,
  useEngagementData,
  useQuiz,
  usePoints,
  useTier,
  useBadges,
  useChallenge,
  useLeaderboard,
} from '@bricqs/sdk-react';

const ENGAGEMENT_ID = 'your-engagement-uuid';

function App() {
  return (
    <BricqsProvider config={{ apiKey: 'bq_live_xxx' }}>
      <CampaignPage />
    </BricqsProvider>
  );
}

function CampaignPage() {
  const { getQuizConfig, getComponent, isLoading } = useEngagementData({
    engagementId: ENGAGEMENT_ID,
  });

  if (isLoading) return <LoadingSpinner />;

  return (
    <div className="campaign-layout">
      <main>
        <QuizSection
          config={getQuizConfig()!}
          componentId={getComponent('quiz')!.id}
        />
      </main>
      <aside>
        <PointsCard />
        <BadgeShelf />
        <ChallengeProgress />
        <TopPlayers />
      </aside>
    </div>
  );
}

function PointsCard() {
  const points = usePoints({ engagementId: ENGAGEMENT_ID });
  const tier = useTier({ engagementId: ENGAGEMENT_ID });

  return (
    <div className="points-card">
      <span className="balance">{points.balance}</span>
      <span className="label">points</span>
      {tier.currentTier && (
        <div className="tier" style={{ color: tier.tierColor || '#666' }}>
          {tier.tierName} — {tier.pointsToNext} pts to {tier.nextTierName}
        </div>
      )}
    </div>
  );
}

function BadgeShelf() {
  const { badges } = useBadges({ engagementId: ENGAGEMENT_ID });

  return (
    <div className="badge-shelf">
      {badges.map(badge => (
        <div key={badge.code} className={badge.earned ? 'earned' : 'locked'}>
          <img src={badge.icon} alt={badge.name} />
          <span>{badge.name}</span>
        </div>
      ))}
    </div>
  );
}

function ChallengeProgress() {
  const ch = useChallenge({ engagementId: ENGAGEMENT_ID, autoEnroll: true });
  if (!ch.challenge) return null;

  return (
    <div className="challenge">
      <h3>{ch.challenge.name}</h3>
      <div className="progress-bar">
        <div style={{ width: ch.progressPercentage + '%' }} />
      </div>
      <p>{ch.completedObjectives}/{ch.totalObjectives} objectives</p>
    </div>
  );
}

function TopPlayers() {
  const lb = useLeaderboard({
    code: 'main',
    engagementId: ENGAGEMENT_ID,
    limit: 5,
  });

  return (
    <ol className="leaderboard">
      {lb.entries.map(e => (
        <li key={e.rank}>
          <span className="rank">#{e.rank}</span>
          <span className="name">{e.name}</span>
          <span className="score">{e.score}</span>
        </li>
      ))}
    </ol>
  );
}

Migrating from iframe to Headless

1
Keep BricqsProvider

If you already use the React SDK, keep the same provider setup. Headless hooks work within the same context.

2
Replace BricqsEngagement with hooks

Replace <BricqsEngagement /> with useEngagementData() + activity hooks. Build your own rendering layer.

3
Move event handlers to hook results

Instead of onPointsAwarded callback props, read from quiz.actionResults.points_awarded after submission.

4
Use progression hooks for sidebar data

Replace any iframe-based progression displays with usePoints(), useBadges(), useChallenge(), and useLeaderboard().

Next Steps