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() useBadgesHeadless() │ │ usePoints() useTier() useChallenge() │ │ useLeaderboard() useRewardsHeadless() │ │ useReferral() useContest() useGates() │ │ useActivityProgress() useActivityCalendar() │ │ │ │ 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>
);
}Hook Reference
Detailed API docs for every hook, organized by category.
useQuiz, useSpinWheel, useForm, usePoll, useSurvey, interactive activity lifecycle from config to submission.
usePoints, useTier, useBadgesHeadless, useChallenge, useLeaderboard, useRewardsHeadless, gamification data with auto-refresh.
useEngagementData, useReferral, useContest, useGates, useActivityProgress, useActivityCalendar.
QuizProvider with render-prop slots, customize the look while keeping built-in state management.
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);
}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,
useBadgesHeadless,
useChallenge,
useLeaderboard,
useReferral,
useContest,
useGates,
useActivityProgress,
useActivityCalendar,
} 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 } = useBadgesHeadless({ 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(), useBadgesHeadless(), useChallenge(), and useLeaderboard().
