Loyalty tiers in headless gamification
A tier is a named status level a user occupies based on cumulative behaviour. Bronze, Silver, Gold. Most loyalty programs reach for tiers because they communicate progress at a glance and let benefits scale with engagement. This page covers what a tier system actually is, how Bricqs evaluates and updates tiers, and the patterns you will reach for when wiring tiers into your product.
- A tier is a named status level a participant occupies based on cumulative behaviour, and every participant is at exactly one tier at a time.
- Compare tiers by the level integer, never by the code string; codes are display labels that can be renamed in the dashboard.
- Anchor tier criteria on lifetime totals so redemption never demotes a participant; tier movement should be monotonic upward.
- Tier evaluation runs in the same code path as points award, so there is no separate cron and a tier_changed fact is emitted on every transition.
- Tier drives UX surfaces like chips and benefits pages; never use a tier gate as the source of truth for paid access or authorization.
What a tier is, in any gamification system
Before the API. Every tier system, regardless of vendor, decomposes into the same primitives. If you understand these, the Bricqs implementation is just one mapping you could make.
A tier is a named level a participant occupies based on satisfying a set of criteria. Tiers are mutually exclusive: a participant is at exactly one tier at a time. Tier membership is dynamic; as criteria change (more points, segment switch, time-based reset) the participant can move up or down.
Engineers wiring tier rendering into product surfaces (header chips, profile pages, benefits screens); growth and lifecycle teams choosing thresholds and benefits. Tiers are owned by product, computed by the platform, surfaced by engineering.
Badges accumulate and never expire; a user collects them. Tiers are exclusive and dynamic: one current tier at a time, with the possibility of moving up or down.
Points are the currency. Tiers are the destination. Most programs use both: points are what the user earns and spends; tiers track lifetime earning.
A segment is a property of the user (premium plan, EU region). A tier is earned through behaviour over time. Segments are inputs; tiers are an output of the engagement loop.
A milestone is a one-time threshold crossing. A tier is an ongoing status with benefits. Crossing a milestone often promotes a tier, but the tier is the steady state.
The decomposition every tier system uses: a definition (the tier as a noun: code, name, level, color, criteria) and a placement (the tier as a verb: this participant is at this tier as of this time, with the next-tier delta shown alongside). Definitions are tenant-scoped configuration. Placements are participant-scoped state that the engine updates whenever a relevant signal changes.
The three operations every API supports: read a participant’s current tier (with next-tier progress), let the engine update tier automatically when criteria change, and override tier manually for edge cases (VIP, support escalation). Most calls are reads; writes are rare.
The hardest design decision is not the schema, it is the evaluation cadence. Two patterns dominate. Continuous evaluation re-evaluates after every relevant event (points earned, segment changed); lowest latency, simplest mental model. Scheduled evaluation runs on a fixed cadence (nightly, weekly); lower cost, harder to debug (“why is my tier still Silver?”). Bricqs uses continuous evaluation on points changes; scheduled re-evaluation is on the roadmap for segment-driven tiers.
When tiers are the right tool, and when they are not
Tiers are heavy. They imply persistent benefits, communications about status changes, and a graceful path for downgrades. Pick the moments where they earn their weight.
You want benefits to scale with engagement
Tiers map naturally to differentiated benefits: better support response time, larger discounts, exclusive features.
- The tier is the contract; the benefits are the consequence.
- Pair with the rewards engine to gate redeemables by tier level.
You want a visible status marker on the profile
Tier names (Bronze, Silver, Gold, Platinum) communicate progress at a glance. Users self-identify with their tier.
- Loss aversion at the threshold drives re-engagement.
- Status chips earn real estate on profile and header surfaces.
You want segmentation derived from behaviour
Tiers are computed from criteria, so they update without manual intervention.
- Email campaigns can filter on tier without maintaining a list.
- Dashboards stay accurate even as the participant base churns.
You want long-term progression beyond points
Points are short-lived (earn, spend, expire). Tiers are the long-term recognition that survives spend events.
- Even when balances are zeroed, the user keeps their tier.
- Lifetime totals drive evaluation, so tier is monotonic.
The recognition should be permanent and additive
That is a badge, not a tier. Tiers are mutually exclusive: getting Gold replaces Silver in the active slot.
You only have one level
A single-tier system is just a flag. If everyone is ‘Member’, a boolean attribute or a single segment will do.
Criteria fire constantly and tier flaps up and down
Flapping (Bronze, Silver, Bronze in the same day) is worse than no tier. Move to a slower-changing metric like quarterly point totals.
Benefits would not differ across levels
Tiers without differentiated benefits are decorative, and decorative tiers train users to ignore the system.
It is a short campaign that ends in two weeks
Tiers are persistent. Use the contest system for time-bound rankings; reserve tiers for the long-running loyalty layer.
- Looking for the marketing-team take? The strategy guide on progression systems covers when tiers fit a program, threshold design, KPIs, and example loyalty stacks.
- Configure in the dashboard: Progression → Tiers → New Tier.
Bricqs models tiers with the standard two-part decomposition above: a definition (the level) and a placement (the participant’s current level). Definitions are created through the dashboard or the admin API. Placements update automatically whenever the participant’s points change, and can also be overridden through a direct API call.
Tier evaluation runs in the same code path as points award, so you never have to schedule a re-evaluation cron. Tier changes emit a fact your downstream systems can subscribe to. The React SDK auto-refreshes when a tier change is broadcast on the client event bus.
What a tier looks like
Two shapes you will see in API responses and SDK hooks. The first is the tier definition (the template). The second is the participant view (current placement plus next-tier delta).
{
"code": "gold", // your code, what you use in API calls
"name": "Gold", // shown in the UI
"level": 3, // ordered level, used for upgrade/downgrade comparison
"color": "#FFD700", // optional, used for accents
"icon_url": "https://cdn.example.com/tiers/gold.png",
"description": "Premium status with extra benefits.",
"criteria": { // what a participant needs to satisfy this tier
"min_points": 5000
},
"next_criteria": { // optional, used to compute next-tier progress
"min_points": 10000
},
"is_active": true // false = paused, will not be assigned
}The level field is what matters for ordering. Codes are opaque strings; level is what the evaluator uses to decide which tier is higher. Always assign distinct ascending integers (1, 2, 3, 4).
criteria is open-ended. Start with min_points; add segment filters or custom rules later. The evaluator returns the highest-level tier whose criteria the participant satisfies.
{
"participant_id": "user_42",
"current_tier": {
"code": "gold",
"name": "Gold",
"level": 3,
"color": "#FFD700",
"achieved_at": "2026-02-01T10:00:00Z"
},
"next_tier": { // null if at top tier
"code": "platinum",
"name": "Platinum",
"points_required": 10000,
"points_remaining": 8500
},
"tier_history": [ // best-effort, last few transitions
{ "code": "bronze", "name": "Bronze", "achieved_at": "2026-01-01T..." },
{ "code": "silver", "name": "Silver", "achieved_at": "2026-01-15T..." },
{ "code": "gold", "name": "Gold", "achieved_at": "2026-02-01T..." }
]
}next_tier is computed live. points_remaining is derived at read time from the participant’s lifetime points and the next tier’s threshold. Changes to thresholds reflect immediately without a batch job.
tier_history is best-effort. Last few transitions, useful for surfacing recent upgrades. For an audit trail, subscribe to the tier-change fact via webhook once outbound delivery ships.
How a tier change happens
One sequence covers both automatic and manual tier changes. The difference is what kicks it off.
- 01
A trigger arrives
Either the participant just earned points (the engine re-evaluates automatically), or your backend calledPOST /api/v1/gamify/tiers/assignto override. - 02
The evaluator picks the highest-eligible tier
For each active tier definition, the evaluator checks whether the participant satisfies the criteria block. The highest-level tier that matches wins. If nothing matches, the participant has no tier. - 03
The engine compares to current tier
If the new tier equals the current tier, the call short-circuits as a no-op (no fact emitted). Re-assigning the same tier twice is safe. - 04
Otherwise, the placement is updated
The participant's current tier is set to the new tier and the transition is appended to tier history. - 05
A tier-change fact is emitted
The factsystem.tier_changed.v1includes the new tier, new level, previous tier and level, and the source of the change (automatic vs assign). - 06
The SDK refreshes
Components usinguseTierlisten to atier:changedevent on the client bus and re-fetch. Once outbound webhook delivery for progression events ships, the tier-change fact will also be delivered to your downstream consumers.
Read and override tiers from any backend
Two endpoints cover almost every case. All endpoints are under /api/v1/gamify/ and authenticate with an API key (bq_live_ for production, bq_test_ for sandbox).
Read a participant’s tier
Returns current tier, next-tier delta, and recent transition history. Read this from your backend when you need to gate a benefit, or call it through the SDK to render a tier chip.
curl https://api.bricqs.com/api/v1/gamify/participants/user_42/tier \
-H "Authorization: Bearer bq_live_xxxxx"
# Response (200 OK)
{
"participant_id": "user_42",
"current_tier": {
"code": "gold",
"name": "Gold",
"level": 3,
"color": "#FFD700",
"achieved_at": "2026-02-01T10:00:00Z"
},
"next_tier": {
"code": "platinum",
"name": "Platinum",
"points_required": 10000,
"points_remaining": 8500
},
"tier_history": [
{ "code": "bronze", "name": "Bronze", "achieved_at": "2026-01-01T..." },
{ "code": "silver", "name": "Silver", "achieved_at": "2026-01-15T..." },
{ "code": "gold", "name": "Gold", "achieved_at": "2026-02-01T..." }
]
}Override a tier manually
Bypass automatic evaluation and pin a participant to a specific tier. Used for VIP promotions, support escalations, or manual corrections. Safe to retry with an Idempotency-Key header.
curl -X POST https://api.bricqs.com/api/v1/gamify/tiers/assign \
-H "Authorization: Bearer bq_live_xxxxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: vip-promo-2026-q2-user_42" \
-d '{
"participant_id": "user_42",
"tier_code": "platinum",
"reason": "VIP promotion (2026 Q2)"
}'
# Response (200 OK)
{
"participant_id": "user_42",
"tier_code": "platinum",
"tier_name": "Platinum",
"level": 4,
"previous_tier_code": "gold"
}Manage tier definitions (admin only)
Most teams create and edit tiers in the dashboard. If you want to script it (e.g. seed tiers in CI), use the admin endpoints. They require an admin-scoped API key.
GET /api/v1/gamify/tiers
GET /api/v1/gamify/tiers/{tier_code}
POST /api/v1/gamify/tiers
{ "code": "gold", "name": "Gold", "level": 3, "criteria": { "min_points": 5000 }, ... }
PATCH /api/v1/gamify/tiers/{tier_code}
{ "name": "Gold Elite" }
DELETE /api/v1/gamify/tiers/{tier_code}Render the tier chip with one hook
The useTier hook returns the current tier, the points to next tier, and the next tier name. It listens to a tier:changed event on the client bus and re-fetches automatically when a tier upgrade lands.
"use client";
import { useTier } from "@bricqs/sdk-react";
export function TierChip({ engagementId }: { engagementId: string }) {
const {
currentTier, // full tier object (or null if no tier yet)
tierName, // shortcut: name string
tierLevel, // shortcut: ordered level
tierColor, // shortcut: brand color
pointsToNext, // points remaining to the next tier
nextTierName, // name of the next tier (null at top)
isLoading,
error,
refresh,
} = useTier({ engagementId });
if (isLoading) return null;
if (error || !currentTier) return null;
return (
<div
className="flex items-center gap-2 rounded-full px-3 py-1 border"
style={{ borderColor: tierColor, color: tierColor }}
>
<span className="font-semibold">{tierName}</span>
{nextTierName && pointsToNext != null && (
<span className="text-xs text-slate-500">
{pointsToNext.toLocaleString()} to {nextTierName}
</span>
)}
</div>
);
}No polling by default. Tier changes are pushed via the client event bus, so the hook only re-fetches when a relevant event arrives. You can still call refresh() after a backend write that should bump the tier.
Render progress with pointsToNext. Combine with the next-tier threshold for a fill percentage. Show a celebration modal only when a tier:changed event fires, not on every render of the chip.
Field shape. Nested objects keep snake_case wire fields (achieved_at, tier_color); top-level shortcuts are camelCase. Prefer the shortcuts where they exist.
Recipes you will reach for
Four patterns that cover most tier implementations in production. Each is a small composition of the primitives above.
Pattern 1: VIP override from your backend
The user qualifies for a tier outside the normal points criteria (e.g. enterprise contract, support escalation, marketing campaign). Skip the evaluator and pin the tier directly. Use an Idempotency-Key derived from the business reason so retries do not produce noise.
// In your contract-signing webhook
await fetch("https://api.bricqs.com/api/v1/gamify/tiers/assign", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.BRICQS_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": `enterprise-contract-${contract.id}`,
},
body: JSON.stringify({
participant_id: contract.account_owner_id,
tier_code: "platinum",
reason: `Enterprise contract ${contract.id}`,
}),
});The Idempotency-Key collapses duplicate retries; the second call returns the original response without emitting a second tier-change fact. If the participant is already at the requested tier, the call is a no-op short-circuit.
Pattern 2: Gate a feature on minimum tier level
Use the level field, not the code, when comparing tiers. Levels are ordered integers; codes are opaque strings. Comparing by level means renaming a tier in the dashboard does not break the gate.
"use client";
import { useTier } from "@bricqs/sdk-react";
const MIN_LEVEL_GOLD = 3;
export function GoldOrAboveOnly({
engagementId,
children,
}: {
engagementId: string;
children: React.ReactNode;
}) {
const { tierLevel, isLoading } = useTier({ engagementId });
if (isLoading) return null;
if (tierLevel == null || tierLevel < MIN_LEVEL_GOLD) {
return <UpgradeCta />;
}
return <>{children}</>;
}Render an upgrade CTA for users below the threshold. Pair this with the points-to-next field to show them how close they are. Never use a tier gate as a security boundary; it is a UX gate, not an authorization gate. Real access control belongs in your backend.
Pattern 3: Trigger downstream side effects when tier changes
Subscribe to the tier-change fact and fan out to your CRM, email, and reward services. Each side effect is independent and safe to retry.
tier.changed.v1) is the next major addition to the platform. Today, the fact is emitted internally but not delivered to your endpoint; poll GET /participants/{id}/tier from your sync job, or react inside a session via the SDK hook’s tier:changed client event. The code below is the shape your handler will take once outbound webhook delivery ships.// POST /webhooks/bricqs, subscribed to tier.changed.v1
export async function POST(req: Request) {
const event = await verifyAndParse(req); // your signature check
const { new_tier, previous_tier } = event.payload;
// Only run for upgrades, not downgrades
if (rankOf(new_tier) <= rankOf(previous_tier)) {
return new Response(null, { status: 200 });
}
await Promise.all([
sendTierUpgradeEmail(event.participant_id, new_tier),
syncTierToCrm(event.participant_id, new_tier),
allocateWelcomeReward(event.participant_id, new_tier),
]);
return new Response(null, { status: 200 });
}Tier-change facts are emitted once per transition (the engine short-circuits no-op assigns), so your handler does not need to guard against duplicate fires for the same upgrade. Still, treat every handler as retry-safe: webhook delivery itself retries on non-2xx responses.
Pattern 4: Downgrade after a period of inactivity
Most loyalty programs decay tier status after a year of inactivity. Run a scheduled job that lists participants whose last activity is older than your decay window, and assign them the lowest-tier code (or no tier).
// List inactive participants from your own analytics
const stale = await db.query(`
SELECT participant_id FROM user_activity
WHERE last_seen_at < NOW() - INTERVAL '12 months'
AND current_tier_level > 1
`);
for (const { participant_id } of stale.rows) {
await fetch("https://api.bricqs.com/api/v1/gamify/tiers/assign", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.BRICQS_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": `inactivity-decay-${participant_id}-${monthKey()}`,
},
body: JSON.stringify({
participant_id,
tier_code: "bronze",
reason: "Inactivity decay",
}),
});
}The monthly Idempotency-Key prevents the same participant from being demoted twice if your job retries. Run it nightly; the first run of each month does the real work and subsequent runs are no-ops. Communicate the decay policy in your tier benefits page so users are not surprised.
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.
Making the tier code your comparison surface. App code reads if (tier === 'gold'). The first time a PM renames Gold to Gold Elite, every gated benefit silently breaks for Gold users.
Design choice: expose level (an ordered integer) as the stable contract; codes are display labels that can change. Build your gating logic against level, document it as the public surface, and treat code renames as cosmetic.
Anchoring tier criteria on spendable points. Spend fluctuates: a user redeems a reward, drops below the threshold, gets demoted mid-session. The program looks like it punishes redemption.
Design choice: tier criteria should anchor on lifetime totals (only-increasing). Redemption never demotes; tier movement is always upward. The Bricqs evaluator defaults to this when min_points is the criterion.
Conflating tier with entitlement ("Gold users get the premium plan"). Tier evaluation runs on engagement-engine timing; paid-access systems need transactional guarantees about who has what.
Design choice: tier drives UX (chip, benefits page, hint cards). Your billing or entitlement system drives access. Subscribe to tier-change events to keep them aligned, but never let tier be the source of truth for paid access.
Numbering tiers with no headroom (level 1, 2, 3 with no gaps). The first time product wants to insert a mid-tier between Silver and Gold, every cached comparison and every dashboard filter that referenced level >= 3 is wrong.
Design choice: number your tiers with gaps (10, 20, 30, 40, ...). Inserting a new level later becomes a config change instead of a renumbering migration. The platform never reads the gaps; your downstream tooling will thank you.
Designing tiers without qualitatively different benefits. Three levels where the only difference is "larger discount" are decorative; users notice the lack of differentiation and the program loses signal.
Design choice: each tier needs at least one benefit that is qualitatively different from the one below (a new feature, a status surface, an exclusive event). If you cannot name one for a tier, the tier should not exist; collapse it into the neighbour.
Where to go next
Concepts, references, and patterns this page anchors. Pick the next read based on what you are about to build.
Points
Points are the most common tier criteria. Covers the ledger, idempotency, and the lifetime vs balance distinction that tier evaluation depends on.
Read itBadges
Badges are recognition; tiers are status. Often paired: a tier upgrade can fire a recognition badge.
Read itProgression overview
How tiers, badges, streaks, points, and milestones fit together.
Read itTiers REST API
Full endpoint detail: every parameter, every response field, every status code.
Read itProgression hooks
useTier reference + the other progression hooks (usePoints, useBadges, useStreaks).
Read itLoyalty tier engine
End-to-end walkthrough composing points, tiers, and rewards into a loyalty engine.
Read itProgression systems
PM/lifecycle perspective: when tiers fit a program, threshold design, KPIs, and example loyalty stacks. Pairs with this dev guide.
Read itAuthentication and API keys
How bq_live_ and bq_test_ keys work, scopes, and which key to use where.
Read itWebhooks
Set up outbound destinations and signature verification. Needed for Pattern 3 once progression-event delivery ships.
Read itCommon questions when integrating
Build with tiers
Pair this with the umbrella strategy guide on progression systems, or jump to the React SDK setup page.
