BricqsBricqs

Headless SDK: engagement components

Quizzes, spins, scratches, and predictions all share one hook: useEngagement. The hook returns the configured experience, the participant's state, and a submit handler that fires the right event with the right idempotency key.

Reading time8 minutes
Last updatedMay 2026

Key takeaways

Quick read
  • One hook (useEngagement) handles every engagement type. Branch on engagement.type for type-specific UI.
  • The hook polls config from the server. Builder edits propagate without a redeploy.
  • submit fires the underlying event with the right idempotency key. Always pass an attemptId for retries.
  • Result data (the spin segment, the quiz score) comes back in the submit response on the sync path.
  • Always render an empty state. Engagements can be paused or deleted server-side.

Quickstart

A working spin in 35 lines

components/SpinWheel.tsx·tsx
"use client";
import { useEngagement } from "@bricqs/headless-react";

export function SpinWheel({ engagementId }: { engagementId: string }) {
  const { engagement, state, submit, isLoading } = useEngagement({
    engagementId,
    sync: true, // resolve the prize in the response
  });

  if (isLoading || !engagement) return null;
  if (state.completed) {
    return <p>You already won {state.lastReward?.label}.</p>;
  }

  async function spin() {
    const result = await submit({
      attributes: {},
      attemptId: `spin:${engagementId}:${Date.now()}`,
    });
    showRevealAnimation(result.outcome.label);
  }

  return (
    <button onClick={spin} className="px-6 py-3 rounded-xl bg-orange-500 text-white">
      Spin to win
    </button>
  );
}

Quiz

A 6-question quiz with branching

components/Quiz.tsx·tsx
"use client";
import { useEngagement } from "@bricqs/headless-react";

export function Quiz({ engagementId }: { engagementId: string }) {
  const { engagement, state, submit, isLoading } = useEngagement({
    engagementId,
    sync: true,
  });

  if (isLoading || !engagement) return null;
  if (engagement.type !== "quiz") return null;

  const q = engagement.questions[state.cursor ?? 0];

  async function answer(choiceId: string) {
    const result = await submit({
      attributes: { question_id: q.id, choice_id: choiceId },
      attemptId: `quiz:${engagementId}:q${q.id}`,
    });

    if (result.completed) {
      window.location.href = `/quiz/result/${result.resultId}`;
    }
  }

  return (
    <article>
      <p className="text-sm text-slate-500">Question {(state.cursor ?? 0) + 1} of {engagement.questions.length}</p>
      <h2 className="text-xl font-bold mb-4">{q.text}</h2>
      <ul className="grid gap-3">
        {q.choices.map((c) => (
          <li key={c.id}>
            <button
              onClick={() => answer(c.id)}
              className="w-full text-left rounded-xl border p-3 hover:border-orange-500"
            >
              {c.label}
            </button>
          </li>
        ))}
      </ul>
    </article>
  );
}

Scratch

Server-determined reveal

tsx
const { state, submit } = useEngagement({ engagementId, sync: true });

async function reveal() {
  // Submit on first scratch. The reveal you animate is the prize the server returned.
  const result = await submit({
    attributes: {},
    attemptId: `scratch:${engagementId}:${userId}`,
  });
  setPrize(result.outcome);
}

The prize is determined server-side at submit time. Animate the reveal to that outcome; never let the client decide what was won.

Common mistakes

What goes wrong

01Mistake

Determining the prize on the client and sending it as an event. Trivially gamed.

Fix

Always sync=true. Server returns the outcome; client animates to that outcome.

02Mistake

Reusing attemptId. Retries silently get the original result, which the user has already seen.

Fix

Generate a fresh attemptId per actual submission. Use Date.now() or a UUID.

03Mistake

Hardcoding the question count. Builder updates break the UI.

Fix

Render from engagement.questions.length. The hook returns live config; builder edits show up without a redeploy.

04Mistake

Submitting on every render. The submit handler is fired in useEffect by mistake.

Fix

submit must be called from a user gesture (click, swipe, scratch). Never from useEffect.

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.

1 brief to align the room2 mechanics max in version one
What happens next
01
Pick the mechanic
Choose the smallest working system for the brief.
02
Launch without rebuilds
Configure rules and rewards in one place.