BricqsBricqs
Concept · Engagement13 min read · For engineers + technical PMs

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.

useChallenge
One hook handles enrol, progress, milestones, leaderboard.
Key takeaways
  • 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.
1. The concept

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.

Definition

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.

Who owns it

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.

How it differs from adjacent mechanics
vs engagements (quiz, spin, form)

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.

vs contests

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.

vs milestones (standalone)

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.

vs tier programs

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.

2. Design decisions

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.

Reach for a challenge when
01Multi-step activation

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.

02Seasonal program

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.

03Bundled goals

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.

04Cohort leaderboard

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.

Do not use a challenge when
01Single action

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.

02Per-action reward

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.

03Competitive prize

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.

04No surface

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.

05Shifting rules

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.

Before you read on
  • 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.

3. Data model

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.

jsonChallenge definition
{
  "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.

jsonParticipant progress response
{
  "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
    }
  ]
}
4. Lifecycle

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: draftscheduled activepaused (optional) → completedarchived. Only active challenges accept enrolments and score progress.

Participation states: enrolledin_progress (first contributing fact) → completed (all required objectives done) or withdrawn (the participant left or the challenge ended without them completing).

  1. 01

    A participant enrols

    Your code calls POST /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.
  2. 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).
  3. 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 any points_on_completion are granted.
  4. 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.
  5. 05

    Completion check

    If all required objectives are now complete, the participation is marked completed and the completion rewards are granted.
  6. 06

    Facts are emitted

    The engine emits progression.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.
RoadmapOutbound webhook delivery for challenge progression events (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.
5. REST API

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.

bashPOST /api/v1/public/challenges/{challenge_id}/enrol
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).

bashGET /api/v1/public/challenges/{challenge_id}/progress
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 shape

Read the leaderboard

Returns top entries plus the calling participant’s rank inside the challenge cohort.

bashGET /api/v1/public/challenges/{challenge_id}/leaderboard
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 }
}
Path roadmapThe challenges endpoints sit under /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.
6. React SDK

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.

tsxcomponents/ChallengePanel.tsx
"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.

7. Common patterns

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.

tsserver.ts (your user-created handler)
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.

jsonObjective with prerequisite
{
  "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.

jsonMilestone block on the challenge definition
{
  "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.

RoadmapOutbound webhook delivery for challenge events (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.
tsjobs/syncChallengeCompletions.ts
// 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.

8. Design pitfalls

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.

Mistake

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.

Fix

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.

Mistake

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.

Fix

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.

Mistake

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.

Fix

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.

Mistake

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.

Fix

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.

Mistake

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.

Fix

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.

Mistake

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.

Fix

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.

FAQ

Common questions when integrating

Wire it up

Build with challenges

Pair this with the strategy guide on challenges, or jump straight to the SDK setup page.