Challenges in headless gamification
A challenge is the longest-form mechanic in the gamification toolbox. It bundles a sequence of goals, a milestone payout map, completion rewards, and a leaderboard into one persistent journey a participant works through over days or weeks. This page covers what a challenge actually is, how Bricqs evaluates progress, and the patterns you will reach for when wiring challenges into your product.
- A challenge is a multi-step program a participant enrols in; it bundles objectives, milestone payouts, completion rewards, and a cohort leaderboard into one persistent journey.
- Challenges decompose into a definition, an enrolment (one record per challenge-participant pair, idempotent on re-enrol), and a progression record that updates as facts land.
- Duration type determines the program shape: rolling for onboarding (clock starts at enrolment), fixed for shared-calendar campaigns, perpetual only for no-urgency content libraries.
- Milestone payouts use stable idempotency keys so a retried fact never double-rewards, and contributing facts are counted exactly once per participation.
- Commit to one objective structure per challenge, either linear with prerequisite_objective_ids or parallel with no prerequisites; mixing the two makes the journey feel arbitrary.
What a challenge is, in any gamification system
Before the API. Every challenge system, regardless of vendor, decomposes into the same primitives. If you understand these, the Bricqs implementation is just one mapping you could make.
A challenge is a named multi-step program a participant enrols in. It has a duration (fixed dates, rolling N days, or perpetual), a set of objectives that count specific behaviours, a milestone map that pays out rewards as progress accumulates, and a leaderboard that ranks enrolled participants. When all objectives are met (or a configured completion criterion fires), the challenge completes and the participant receives the completion rewards.
Engineers wiring challenge surfaces (challenge card, objective tracker, milestone map, leaderboard); product managers designing the journey shape and reward economy. Challenges are the most common place a PM and an engineer split work cleanly: the PM picks the objectives and rewards in the dashboard; the engineer renders the progress in the product.
An engagement is a single moment: open it, do it, get a result. A challenge tracks aggregate progress across many moments (often many engagements). Quizzes feed challenges, but the quiz is the action and the challenge is the journey.
A contest is a time-bound competition with prizes, fraud detection, and tie-breaking. A challenge is a personal program with progression rewards. Use a contest when there are winners and losers; use a challenge when everyone who completes wins.
A standalone milestone is one threshold ("hit 1000 referrals"). A challenge bundles a sequence of milestones with rewards, a duration, enrolment, and a leaderboard. Use a milestone when the threshold is enough; use a challenge when the story matters.
A tier ranks lifetime contribution. A challenge sets a time-bound program with a defined endpoint. Use tiers for long-term recognition; use challenges for seasonal or campaign-shaped journeys.
The decomposition every challenge system uses: a definition (the challenge as a noun: code, name, duration, objectives, milestones, completion rewards), an enrolment (the participant opts in or is auto-enrolled), and a progression record (per-participant progress against each objective, milestones reached, score, rank). Definitions are tenant-scoped configuration. Enrolments are participant-scoped records. Progression updates as relevant actions land.
The two operations every API supports: enrol a participant in a challenge, and read a participant’s progress (objectives, milestones, score, rank). Everything else is configuration in the dashboard or admin endpoints.
The hardest design decision is not the schema, it is the objective shape. Two questions you have to answer up front. (1) What counts? A login? A purchase over £20? A quiz with a 70+ score? The evaluator type maps directly to this: activity_count for “did the action”, score_threshold for “did the action well”, unique_days for “did the action over time”. (2) Does order matter? Objective prerequisites turn a challenge into a linear path (one step locks the next); leaving them open makes the challenge a buffet of parallel goals. Linear is more satisfying; parallel is more flexible. Pick before launch and communicate it.
When challenges are the right tool, and when they are not
Challenges are heavy: they need design, dashboarding, comms, and a reward economy. Pick the moments where the journey shape genuinely improves the experience.
The activation story is multi-step
First-week activation, fitness onboarding, course completion. A challenge gives the participant a visible map of what is next instead of a series of one-off pushes.
You want a seasonal program with a beginning and end
A 30-day fitness challenge, a 2-week reading streak, a quarterly sales target. The fixed-duration shape creates urgency without rebuilding the program every season.
Different behaviours all count toward one bigger goal
Complete three quizzes, earn 500 points, log in five days. The challenge bundles disparate objectives so the participant sees one program instead of three.
You want a per-cohort leaderboard tied to the program
Challenges ship with a leaderboard scoped to the enrolled participants. Useful for class-style or team-style programs where the comparison group is the people in the journey, not the whole tenant.
It is a single action
If the participant does one thing and gets a reward, that is an engagement (quiz, spin, form) with a completion action, not a challenge. Challenges have overhead; reserve them for journeys.
The reward should be issued instantly per action
Per-action point grants are an event-driven flow (your code sends an event, points award). A challenge aggregates across many actions and pays out at milestones; if there is no aggregation, skip it.
The audience is competitive and the prize is real
Use a contest. Contests have fraud detection, tie-breaking, prize allocation with budget caps, and disqualification handling. Challenges have progression rewards for everyone who completes.
You cannot dashboard the program
Challenges live and die by the visibility surfaces (objective tracker, milestone map). If you cannot afford the UI work to render progress, the challenge feels invisible and engagement collapses.
Objectives change mid-flight
Challenge definitions are versioned; changing objectives after participants are enrolled creates confusion and replays. If the rules need to evolve, end the current challenge and launch a new version.
- Looking for the marketing-team take? The strategy guide on challenges covers when challenges fit a program, journey design, KPIs, and example campaigns.
- Configure in the dashboard: Engagements → Challenges → New Challenge.
Bricqs models challenges with the standard three-part decomposition above: a definition (the program), an enrolment (the participant’s entry into the program), and a progression record (live progress against each objective). You create definitions through the dashboard or the admin API. Participants enrol through a public endpoint (or get auto-enrolled by your code). Progression updates as relevant events land.
Bricqs handles the heavy parts for you. The rules engine listens for events your participants generate (behaviour, reward, system facts) and updates the right objectives. Each fact is counted exactly once per participation; milestone payouts use stable keys so a retried event never pays out twice. Rewards route through the points, badge, and tier services with the same idempotency guarantees as direct calls.
What a challenge looks like
Three shapes you will see in API responses. The first is the definition (the program template). The second is the progression response (what a participant sees). The third is the leaderboard.
{
"id": "ch_uuid_123",
"code": "first_week_activation",
"name": "First Week Activation",
"description": "Complete your onboarding in your first 7 days.",
"challenge_type": "onboarding",
"duration_type": "fixed", // fixed | rolling | perpetual
"duration_days": 7,
"start_date": "2026-06-01T00:00:00Z",
"end_date": "2026-06-30T23:59:59Z",
"engagement_id": "eng_uuid_456", // null = applies across the tenant
"completion_rewards": {
"points": 500,
"badges": ["activated"],
"tier_unlock": null
},
"objectives": [
{
"code": "complete_profile",
"name": "Complete your profile",
"objective_type": "activity_count",
"criteria": { "activity_code": "profile_completed" },
"target_value": 1,
"points_on_completion": 50,
"is_required": true,
"display_order": 1
},
{
"code": "log_in_three_days",
"name": "Log in on 3 different days",
"objective_type": "unique_days",
"criteria": { "activity_code": "session_started" },
"target_value": 3,
"points_on_completion": 100,
"display_order": 2
}
],
"milestones": [
{ "name": "Halfway", "milestone_type": "percentage", "at_percentage": 50,
"rewards": { "points": 100, "badges": ["halfway"] } },
{ "name": "Complete", "milestone_type": "percentage", "at_percentage": 100,
"rewards": { "points": 200, "badges": ["finisher"], "unlock_tier": "silver" } }
],
"status": "active"
}Pick your codes carefully. The code field on a challenge and on each objective is the stable identifier you reference in API calls and analytics. It cannot be changed without re-issuing every related record. Use lowercase snake_case.
Duration type changes everything. fixed uses calendar start/end dates (campaign-shaped), rolling counts N days from each participant’s enrolment (great for onboarding), and perpetual never ends (use sparingly; perpetual challenges create stale progress that nobody finishes).
engagement_id scopes the source. When set, only facts generated by that engagement count toward the challenge. Leave it null to count facts from any source.
{
"participation_id": "part_uuid_789",
"status": "in_progress",
"progress_percentage": 50,
"current_score": 75,
"objectives_completed": 1,
"objectives_total": 2,
"enrolled_at": "2026-06-01T10:00:00Z",
"last_activity_at": "2026-06-03T14:22:00Z",
"completed_at": null,
"rank": 12,
"objectives": [
{
"code": "complete_profile",
"name": "Complete your profile",
"current_value": 1,
"target_value": 1,
"is_completed": true,
"points_earned": 50
},
{
"code": "log_in_three_days",
"name": "Log in on 3 different days",
"current_value": 1,
"target_value": 3,
"is_completed": false,
"points_earned": 0
}
]
}How a challenge progresses
The challenge itself goes through admin states; each participant's journey goes through participation states. Both are independent and run in parallel.
Challenge admin states: draft → scheduled → active → paused (optional) → completed → archived. Only active challenges accept enrolments and score progress.
Participation states: enrolled → in_progress (first contributing fact) → completed (all required objectives done) or withdrawn (the participant left or the challenge ended without them completing).
- 01
A participant enrols
Your code callsPOST /challenges/{id}/enrol, or the participant clicks an enrol button that hits the same endpoint, or your engagement runtime auto-enrols on campaign open. The result is one participation record per (challenge, participant) pair. - 02
A contributing event lands
The participant does something that matches one of the challenge's objective criteria (logs a session, completes a quiz, makes a purchase, earns enough points). - 03
The rules engine evaluates
For each participation that has an objective matching this event, the engine increments the objective’s current value. If the objective just hit its target, it is marked complete and anypoints_on_completionare granted. - 04
Milestone check
The engine recomputes progress percentage / objective count / points total and checks each milestone. If a milestone just crossed its threshold, its rewards (points, badges, tier unlock) are granted with deterministic idempotency keys so retries never double-pay. - 05
Completion check
If all required objectives are now complete, the participation is markedcompletedand the completion rewards are granted. - 06
Facts are emitted
The engine emitsprogression.challenge_progress_updated.v1,progression.milestone_reached.v1, and (on completion)progression.challenge_completed.v1. The React SDK re-fetches automatically on the linked client event.
challenge.objective_completed.v1, challenge.completed.v1) is on the platform roadmap. Today, facts are emitted internally; the SDK hook reacts to them in the participant session. For cross-system sync, poll /challenges/{id}/progress from your job runner.Enrol and read progress from any backend
Three endpoints cover almost every case. All endpoints are under /api/v1/public/challenges/ today; the planned move to /api/v1/gamify/challenges/ is part of the platform namespace consolidation.
Enrol a participant
Creates one participation record per (challenge, participant). Idempotent on the composite: re-enrolling returns the existing record without resetting progress.
curl -X POST https://api.bricqs.com/api/v1/public/challenges/ch_uuid_123/enrol \
-H "Authorization: Bearer bq_live_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"participant_id": "user_42",
"participant_attributes": {
"signup_source": "homepage"
}
}'
# Response (200 OK)
{
"participation_id": "part_uuid_789",
"status": "enrolled",
"enrolled_at": "2026-06-01T10:00:00Z"
}Read progress
Returns the participant’s progress on every objective, their score, their rank, and the milestones they have reached. Use this to render the challenge surface (objective tracker, milestone map, progress bar).
curl 'https://api.bricqs.com/api/v1/public/challenges/ch_uuid_123/progress?participant_id=user_42' \
-H "Authorization: Bearer bq_live_xxxxx"
# Response (200 OK), see the data-model section above for the full shapeRead the leaderboard
Returns top entries plus the calling participant’s rank inside the challenge cohort.
curl 'https://api.bricqs.com/api/v1/public/challenges/ch_uuid_123/leaderboard?participant_id=user_42&limit=10' \
-H "Authorization: Bearer bq_live_xxxxx"
# Response (200 OK)
{
"entries": [
{ "rank": 1, "participant_id": "user_99", "display_name": "Alex", "score": 200 },
{ "rank": 2, "participant_id": "user_42", "display_name": "Jordan", "score": 175 }
],
"my_rank": { "rank": 2, "score": 175, "total_participants": 248 }
}/public/challenges/ today and are scheduled to move under /gamify/challenges/ as part of the participant-data namespace consolidation. The SDK hook abstracts the path; if you use the React hook you do not need to track the migration.Render a challenge with one hook
The useChallenge hook handles enrolment, progress polling, milestone state, and the leaderboard in one call. Pass autoEnroll: true to enrol the participant on first mount.
"use client";
import { useChallenge } from "@bricqs/sdk-react";
export function ChallengePanel({ engagementId }: { engagementId: string }) {
const {
challenge, // the definition
objectives, milestones, // shortcut accessors
isEnrolled, enroll,
progress, // full progression record
progressPercentage,
objectiveProgress, // per-objective current/target/is_completed
completedObjectives, totalObjectives,
milestonesReached,
leaderboard, myRank,
isLoading, error, refresh,
} = useChallenge({ engagementId, autoEnroll: true, refreshInterval: 30000 });
if (isLoading || !challenge) return <p>Loading challenge…</p>;
if (error) return <p>Could not load challenge.</p>;
return (
<article>
<header>
<h2>{challenge.name}</h2>
<p>{challenge.description}</p>
</header>
<progress value={progressPercentage} max={100} />
<p>
{completedObjectives} of {totalObjectives} objectives complete
{myRank && ` · rank ${myRank.rank} of ${myRank.total_participants}`}
</p>
<ol>
{objectiveProgress.map((o) => (
<li key={o.code} className={o.is_completed ? "done" : ""}>
{o.name}: {o.current_value} / {o.target_value}
</li>
))}
</ol>
</article>
);
}autoEnroll: true is a common default. If your campaign opens the challenge for anyone who visits, auto-enrolling on first render gives a one-click experience. Pass autoEnroll: false and call enroll() from a button click when you want explicit opt-in.
The hook also reacts to activity events. When the user completes a quiz or other engagement inside the same session, the hook receives the client event and re-fetches progress immediately, so the objective tracker updates without waiting for the next poll tick.
Field shape note. Objective entries use snake_case wire fields (current_value, is_completed); top-level shortcuts are camelCase (progressPercentage, completedObjectives). A consistent camelCase boundary at the SDK is on the roadmap.
Recipes you will reach for
Four patterns that cover most challenge implementations in production. Each is a small composition of the primitives above.
Pattern 1: Rolling onboarding challenge
Every new user gets the same 7-day journey from their own signup moment. Use duration_type: rolling so each participant's clock starts at their enrolment, not at a fixed campaign date. Auto-enrol the user on first app session.
export async function onUserCreated(user: User) {
// existing post-signup logic...
// Enrol them in the rolling onboarding challenge
await fetch(
`https://api.bricqs.com/api/v1/public/challenges/${ONBOARDING_CHALLENGE_ID}/enrol`,
{
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.BRICQS_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
participant_id: user.id,
participant_attributes: {
signup_source: user.signup_source,
},
}),
}
);
}Re-running enrolment for the same participant is a no-op; the participation row already exists and the call returns it unchanged. Safe to wire into a signup retry path.
Pattern 2: Linear path with prerequisites
Some journeys make sense as a linear path: step 1 unlocks step 2, which unlocks step 3. Each objective declares its prerequisite_objective_ids; the rules engine only scores objectives whose prerequisites are met. The UI can hide the locked steps until they unlock, or render them dimmed.
{
"code": "first_purchase",
"name": "Make your first purchase",
"objective_type": "activity_count",
"criteria": { "activity_code": "purchase_completed" },
"target_value": 1,
"prerequisite_objective_ids": ["complete_profile"],
"points_on_completion": 100,
"display_order": 3
}Use prerequisites to model tutorial-shaped journeys. Avoid mixing prerequisite-gated and parallel objectives in the same challenge; pick one mental model so the participant always knows what is next.
Pattern 3: Mid-challenge milestones with badges
Milestones give the participant something to celebrate before completion. A 50% milestone with a halfway badge, a 100% milestone with the finisher badge. Configure them on the challenge definition; the rules engine pays out the rewards with stable keys so retries do not double-pay.
{
"milestones": [
{
"name": "Halfway",
"milestone_type": "percentage",
"at_percentage": 50,
"rewards": {
"points": 100,
"badges": ["halfway"]
}
},
{
"name": "Complete",
"milestone_type": "percentage",
"at_percentage": 100,
"rewards": {
"points": 200,
"badges": ["finisher"],
"unlock_tier": "silver"
}
}
]
}Milestones can also fire on objective_count (after N objectives done) or points_total (after the participant has earned N points from the challenge). Mix and match if it helps the journey shape.
Pattern 4: Sync completions to your CRM via polling
Until outbound webhook delivery for challenge events ships, poll the progress endpoint from a background job and sync completions to your CRM. The participation status field transitions to completed exactly once per participant, so a daily diff is enough.
challenge.completed.v1) is on the platform roadmap. When it ships, replace the polling pattern below with a webhook handler; the data shape will be the same.// Daily job: find participants who just completed and sync to CRM
const lastSync = await store.lastSyncedAt("challenge_completions");
const participants = await listEnrolledParticipants(CHALLENGE_ID);
for (const pid of participants) {
const res = await fetch(
`https://api.bricqs.com/api/v1/public/challenges/${CHALLENGE_ID}/progress?participant_id=${pid}`,
{ headers: { Authorization: `Bearer ${process.env.BRICQS_API_KEY}` } }
);
const progress = await res.json();
if (
progress.status === "completed" &&
progress.completed_at > lastSync
) {
await crm.recordChallengeCompletion(pid, progress);
}
}
await store.setLastSyncedAt("challenge_completions", new Date());When outbound webhooks for challenge events ship, replace this pattern with a webhook handler. The data shape will be the same.
Design choices teams regret later
Each pitfall is a structural decision you make at design time. Catch them in the dashboard and the spec, not after the program is live.
Setting target_value to a number nobody can realistically hit ("earn 100,000 points in 7 days"). Enrolment looks healthy; completions are zero; the program reads as a failure.
Design choice: model the target against your observed activity. Sample the top 10% of existing users; their numbers tell you what is reachable. The top of the participation curve should hit the milestone in the first half of the duration; everyone else trails.
Picking the wrong duration type for the program shape. A fixed campaign window for a journey that should start at signup, or a perpetual challenge dressed up as a seasonal push.
Design choice: rolling for onboarding (the clock starts at the participant's enrolment); fixed for campaigns (calendar dates everyone shares); perpetual only for content libraries where there is no urgency. Pick before launch; switching duration types mid-flight invalidates every participation.
Using a perpetual challenge as a substitute for a tier program. Participants do not return because the goal feels infinite, and the program never produces a celebration moment.
Design choice: use a tier for ongoing recognition; use a challenge for a defined journey. Perpetual challenges work for "learn at your own pace" content programs but rarely for engagement campaigns. If the program has no endpoint, it should not be a challenge.
Mixing prerequisite-gated and parallel objectives in the same challenge. Some steps unlock the next; others sit open. The participant cannot tell which mental model they are in and the program feels arbitrary.
Design choice: commit to one structure. Linear (every objective declares its prerequisite, the UI renders locked steps dimmed) or parallel (no prerequisites, a buffet of goals the participant can attack in any order). Pick before launch; do not blend.
Designing a challenge as a leaderboard with no objective tracker. The user sees a number (their rank) but no story about how to improve it, so the leaderboard reads as a scoreboard for someone else's game.
Design choice: always pair the leaderboard with the objective tracker. The leaderboard answers "where am I?"; the objective tracker answers "what do I do next?". Both are needed; a challenge surface without the tracker is incomplete.
Auto-enrolling participants without telling them. A challenge surface appears in the app overnight, they do not understand it, they ignore it, and the program reads as low-engagement when the real problem is silent onboarding.
Design choice: pair auto-enrolment with a one-time orientation modal that names the challenge, the reward, and where to find progress. If the orientation budget is not there, use explicit enrolment with a clear call to action instead.
Where to go next
Concepts, references, and patterns this page anchors. Pick the next read based on what you are about to build.
Contests
When everyone-who-completes-wins is not enough and you need ranked winners, fraud detection, and prize allocation, use the contests system instead.
Read itPoints
Most challenges reward points on objective completion and at milestones. Covers the ledger model and idempotent grants.
Read itBadges
Milestone rewards often include badges (halfway, finisher). Covers definitions, awards, and the three trigger patterns.
Read itTier
Challenge completion can unlock a tier; tier rules can in turn shape which challenges show up. Read this to understand the tier evaluation lifecycle.
Read itCommon questions when integrating
Build with challenges
Pair this with the strategy guide on challenges, or jump straight to the SDK setup page.
