Pattern: onboarding flow
A complete onboarding challenge from configuration to completion. Five steps, three reward beats, server-side scoring, client-side rendering. Copy this file, change the names, ship.
Key takeaways
Quick read- Configure the challenge once via the admin API. Render it with a single React hook.
- Five-step shape: profile, first action, connect, invite, outcome.
- Reward at step 1 (badge), at step 3 (free shipping), at completion (loyalty enrolment plus 500 INR voucher).
- Drop-off recovery via webhook to your ESP. No client-side scheduling needed.
- Total integration time: a working day if you already have auth and a CRM.
Anatomy
What you are building
API
POST /admin/challenges to create the challenge. POST /events when the user completes a step. Webhooks fire on completion.
SDK
useChallenge({ challengeId: 'onboarding_v3' }) renders the live state. submitEvent advances objectives optimistically.
User sees
A 5-step checklist on the home screen, ticking off as they act. Reward moments at step 1, step 3, and completion. Skip option in the corner.
Step 1: configuration
Create the challenge once
Run this once per environment. The challenge id is what your client code references.
curl -X POST https://api.bricqs.co/api/v1/admin/challenges \
-H "Authorization: Bearer bq_live_admin_..." \
-H "X-Bricqs-Tenant: tenant_brand_xyz" \
-H "Content-Type: application/json" \
-d '{
"id": "onboarding_v3",
"title": "Get started in 5 steps",
"window_days": 14,
"auto_enrol_on_signup": true,
"objectives": [
{
"id": "complete_profile",
"label": "Complete your profile",
"evaluator": "completion",
"event_type": "profile_completed",
"reward": { "type": "badge", "id": "welcome_explorer" }
},
{
"id": "first_purchase",
"label": "Place your first order",
"evaluator": "completion",
"event_type": "purchase_completed"
},
{
"id": "free_shipping_unlocked",
"label": "Unlock free shipping for life",
"evaluator": "completion",
"event_type": "internal:milestone_3",
"reward": { "type": "perk", "id": "free_shipping_lifetime" }
},
{
"id": "invite_a_friend",
"label": "Invite a friend",
"evaluator": "completion",
"event_type": "referral_sent"
},
{
"id": "second_purchase",
"label": "Make your second order",
"evaluator": "completion",
"event_type": "purchase_completed",
"min_count": 2
}
],
"completion_reward": {
"type": "voucher",
"value": 500,
"currency": "INR"
},
"drop_off_recovery": {
"webhook_at": ["72h_no_progress", "168h_incomplete"]
}
}'Auto-enrol on signup means every new user enters the challenge automatically. The window_days is the deadline.
Step 2: server
Send events as the user acts
Most events are already firing in your stack. You just need to forward them with an idempotency key.
export async function emitToBricqs(
participantId: string,
eventType: string,
attributes: Record<string, unknown>,
idempotencyKey: string
) {
await fetch("https://api.bricqs.co/api/v1/ingest/events", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.BRICQS_ADMIN_KEY!}`,
"X-Bricqs-Tenant": process.env.BRICQS_TENANT!,
"Content-Type": "application/json",
},
body: JSON.stringify({
participant_id: participantId,
event_type: eventType,
attributes,
idempotency_key: idempotencyKey,
}),
});
}import { emitToBricqs } from "@/lib/bricqs";
export async function POST(request: Request) {
const userId = await getCurrentUserId();
const profile = await request.json();
await saveProfile(userId, profile);
// Forward to Bricqs. Idempotent: same user, same event, once.
await emitToBricqs(
userId,
"profile_completed",
{ has_avatar: !!profile.avatar },
`p_${userId}:profile_completed`
);
return Response.json({ ok: true });
}Step 3: client
Render the challenge with one hook
The hook returns objective state, progress, and a completion callback. Polling and reconciliation are built in.
"use client";
import { useChallenge } from "@bricqs/headless-react";
const STEP_CTAS: Record<string, { label: string; href: string }> = {
complete_profile: { label: "Edit profile", href: "/account/profile" },
first_purchase: { label: "Browse the catalog", href: "/shop" },
invite_a_friend: { label: "Invite a friend", href: "/refer" },
};
export function OnboardingChallenge() {
const {
challenge,
objectives,
progressPercent,
isCompleted,
isLoading,
onCompleted,
} = useChallenge({ challengeId: "onboarding_v3" });
onCompleted((event) => {
window.location.href = `/welcome/done?reward=${event.reward.id}`;
});
if (isLoading) return null;
if (isCompleted) return null;
return (
<aside className="rounded-2xl border bg-white p-6">
<header className="flex items-baseline justify-between mb-3">
<h2 className="font-bold text-lg">{challenge.title}</h2>
<span className="text-sm text-slate-500">
{progressPercent}% done · {challenge.daysRemaining} days left
</span>
</header>
<ProgressBar percent={progressPercent} />
<ol className="mt-4 space-y-2">
{objectives.map((o) => {
const cta = STEP_CTAS[o.id];
return (
<li
key={o.id}
className={`flex items-center gap-3 ${o.isComplete ? "opacity-60" : ""}`}
>
<span>{o.isComplete ? "✓" : "○"}</span>
<span className="flex-1">{o.label}</span>
{!o.isComplete && cta && (
<a className="text-blue-600 text-sm" href={cta.href}>{cta.label}</a>
)}
</li>
);
})}
</ol>
</aside>
);
}Step 4: recovery
Drop-off recovery via webhook
Bricqs fires a webhook when a participant has been inactive for 72 hours or has not completed by 168 hours. Wire it to your ESP.
import crypto from "crypto";
export async function POST(req: Request) {
const body = await req.text();
const signature = req.headers.get("x-bricqs-signature");
if (!verifySignature(body, signature)) {
return new Response("Invalid signature", { status: 401 });
}
const event = JSON.parse(body);
if (event.type === "challenge.drop_off_at_72h") {
await sendRecoveryEmail(event.data.participant_id, {
challenge: event.data.challenge_id,
next_step: event.data.next_objective_label,
});
}
return Response.json({ ok: true });
}
function verifySignature(body: string, signature: string | null) {
if (!signature) return false;
const expected = crypto
.createHmac("sha256", process.env.BRICQS_WEBHOOK_SECRET!)
.update(body)
.digest("hex");
return signature === expected;
}HMAC verify before acting on the webhook. Anything else is exposing your ESP to spoofing.
Step 5: completion
The handoff moment
Auto-claim the reward
If the completion reward is a voucher, claim the code in the onCompleted handler so the user lands on a success screen with the code already in hand.
Route to the next thing
Onboarding done is a launchpad. Hand off to the first habit (a streak), the loyalty program, or the home dashboard with a 'mission complete' moment.
onCompleted(async (event) => {
// 1. Claim the voucher server-side
const res = await fetch("/api/bricqs/claim-reward", {
method: "POST",
body: JSON.stringify({ rewardId: event.reward.id }),
});
const { code } = await res.json();
// 2. Show the celebration screen
router.push(`/welcome/done?code=${code}`);
});Pre-launch checklist
Before you ship
Configuration
[ ] Challenge created in admin (POST /admin/challenges)
[ ] Window length set to 14 days
[ ] Auto-enrol on signup enabled
[ ] Drop-off webhook URLs configured
Server
[ ] BRICQS_ADMIN_KEY set in production env (server-only)
[ ] BRICQS_WEBHOOK_SECRET set in production env
[ ] All five qualifying actions emit events with idempotency keys
[ ] Webhook handler verifies HMAC before acting
Client
[ ] BricqsProvider wraps the authenticated tree
[ ] Participant token minted server-side, refreshed at 80% TTL
[ ] OnboardingChallenge component rendered on home screen
[ ] Skeleton fallback during isLoading
[ ] Hidden when isCompleted is true
Rollout
[ ] Verified on a real test account end-to-end
[ ] First step completes in under 60 seconds for a fresh user
[ ] Recovery email lands in inbox at 72h test
[ ] Completion reward claims successfully on a fresh accountDeveloper FAQ
Common questions when integrating gamification with Bricqs.
Ready to ship?
Wire it up with the Bricqs SDK or API
Headless SDK for React UIs, REST API for any backend. Same engine behind both.
