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 retryuseSpinWheel
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 spinwheel.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 valuesusePoll
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() // ResetuseSurvey
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 firesuseTier
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 refreshuseBadges
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 eventsuseChallenge
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 eventsuseLeaderboard
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 refreshuseRewards
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 eventsServer-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);
}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
If you already use the React SDK, keep the same provider setup. Headless hooks work within the same context.
Replace <BricqsEngagement /> with useEngagementData() + activity hooks. Build your own rendering layer.
Instead of onPointsAwarded callback props, read from quiz.actionResults.points_awarded after submission.
Replace any iframe-based progression displays with usePoints(), useBadges(), useChallenge(), and useLeaderboard().
