Headless SDK: rewards
useRewards returns the participant's earned, claimable, and redeemed rewards. claim() turns an earned reward into a redemption. The hook handles expiry, partial states, and idempotency.
Key takeaways
Quick read- useRewards returns earned (claimable) and redeemed lists. Status is per-reward.
- Call claim(rewardId) to issue a coupon code or trigger a fulfilment. The result includes the code and expiry.
- Always show expiry. Hidden expiry is the most common reward complaint.
- claim is idempotent on the SDK side: clicking twice returns the same code.
- For physical rewards (merchandise, samples), claim only collects fulfilment data; shipping happens server-side.
Render rewards
Earned and redeemed
"use client";
import { useRewards } from "@bricqs/headless-react";
export function MyRewards() {
const { earned, redeemed, claim, isLoading } = useRewards();
if (isLoading) return null;
return (
<section className="grid gap-6">
<div>
<h3 className="font-bold mb-3">Ready to claim</h3>
{earned.length === 0 ? (
<p className="text-slate-500">Nothing yet.</p>
) : (
<ul className="grid gap-3">
{earned.map((r) => (
<li key={r.id} className="rounded-xl border p-4 flex items-center gap-4">
<div className="flex-1">
<strong>{r.label}</strong>
<p className="text-sm text-slate-500">{r.description}</p>
<small className="text-slate-400">
Expires {r.expiresAt}
</small>
</div>
<button
onClick={async () => {
const result = await claim(r.id);
showCode(result.code);
}}
className="px-4 py-2 rounded-lg bg-orange-500 text-white"
>
Claim
</button>
</li>
))}
</ul>
)}
</div>
{redeemed.length > 0 && (
<details>
<summary className="font-bold cursor-pointer">Already redeemed ({redeemed.length})</summary>
<ul className="mt-3 grid gap-2">
{redeemed.map((r) => (
<li key={r.id} className="text-sm text-slate-600">
{r.label} · {r.code} · used {r.redeemedAt}
</li>
))}
</ul>
</details>
)}
</section>
);
}Claim
What claim() returns by reward type
// Coupon and voucher: server returns a one-time-use code
const r = await claim("rwd_42");
// r = { code: "WELCOME-Q7P2", expiresAt: "2026-05-30T23:59:59Z", url: null }
// Perk (free shipping, member pricing): server applies it on the account
const r = await claim("rwd_freeship");
// r = { code: null, expiresAt: null, url: null, applied: true }
// Physical: server records intent, fulfilment runs separately
const r = await claim("rwd_sample_kit", {
shipping: { name, addressLines, city, postalCode, country }
});
// r = { code: null, fulfilmentId: "ful_91xx", trackingUrl: null }Common mistakes
What goes wrong
Claim happens on render. The reward is auto-claimed before the user sees it.
Bind claim to a user gesture (button click). Never call from useEffect.
Showing the code as plaintext in the URL. Crawlers steal it.
Show the code in the UI. Send it via a separate authenticated email; never put codes in URLs.
Hiding expiry. Users discover the reward is dead at checkout.
Render expiresAt on the earned card and on the redeemed list. Send a 7-day reminder via webhook.
Calling claim twice on a slow click. The user thinks two rewards were issued.
Disable the button after click. claim is idempotent server-side; the second call returns the same code.
Developer 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.
