# Hatched documentation — full text > Hatched is the gamification layer for B2B products: a typed SDK (@hatched/sdk-js), embeddable widgets, and an HTTP API. Send product events, grow buddies, award coins / tokens / badges / streaks, run marketplaces, and embed it all with a few lines of HTML. Generated from https://docs.hatched.live. Page index: https://docs.hatched.live/llms.txt # Hatched documentation > Drop a buddy into your product in under 10 minutes. Server-only SDK, scoped browser tokens, auto-retries, idempotency built in. Source: https://docs.hatched.live/docs Hatched is the motivation layer for B2B products. Events go in, buddies grow. The Octalysis Planner scores eight motivational drives live; the Mission Anchor, Hatch Ceremony, and LEAGUES seasons are the surfaces your workspace runs on. These docs are organised by task — pick the one that matches your mode. ## Thirty-second taste ```ts import { HatchedClient } from '@hatched/sdk-js'; const hatched = new HatchedClient({ apiKey: process.env.HATCHED_API_KEY!, }); // 1. create a buddy (Greenwave Learning Co. — a new instructional designer) const egg = await hatched.eggs.create({ userId: 'user_designer_priya' }); await hatched.eggs.updateStatus(egg.eggId, 'ready'); const op = await hatched.eggs.hatch(egg.eggId); const finished = await hatched.operations.wait(op.operationId); // 2. teach the buddy about your product await hatched.events.send({ eventId: 'lesson_module_review_user_designer_priya_2026_05_03', userId: 'user_designer_priya', type: 'lesson_completed', properties: { score: 94 }, }); // 3. mint a browser token and embed the widget const session = await hatched.widgetSessions.create({ buddyId: finished.result.buddyId, userId: 'user_designer_priya', scopes: ['read', 'events:track'], ttlSeconds: 900, }); ``` Full walkthrough: [Getting started](/docs/guides/getting-started). ## Pick your track - **[Concepts](/docs/concepts/overview)** — buddies, skills, coins, streaks, badges, paths, marketplace, evolution, rule engine, auth model. - **[Guides](/docs/guides/getting-started)** — getting started, SDK quickstart, web + React Native widgets, events, webhooks, Next.js / Express / edge runtimes, troubleshooting. - **[Reference](/docs/reference/http-api)** — HTTP endpoints, the JavaScript and React Native SDK surfaces, widget props, webhook payloads, error codes, rate limits, changelog. - **[Billing](/docs/billing/pricing)** — plans, credits, Stripe billing portal, 402 handling, and credit top-ups. ## Building with an AI assistant? These docs are machine-readable. Drop our [`AGENTS.md`](https://docs.hatched.live/AGENTS.md) into your repo, point your assistant at [`/llms-full.txt`](/llms-full.txt), and grab any page as Markdown with the buttons under its title — full walkthrough in [Use Hatched with AI coding assistants](/docs/ai-assistants). ## Popular pages - [Getting started (10 min)](/docs/guides/getting-started) - [Widget integration](/docs/guides/widget-integration) - [React Native integration](/docs/guides/react-native-integration) - [Handle webhooks](/docs/guides/handle-webhooks) - [Auth model — secret vs publishable keys](/docs/concepts/auth-model) - [Use Hatched with AI coding assistants](/docs/ai-assistants) - [Error codes](/docs/reference/error-codes) - [Troubleshooting](/docs/guides/troubleshooting) ## What's new Always see the latest SDK release notes at [Changelog](/docs/reference/changelog) — mirrored from the package itself, produced on every merge. --- # Use Hatched with AI coding assistants > These docs are machine-readable. Point Cursor, Claude Code, Copilot, or any LLM at the right files and drop a ready-made instruction file into your repo. Source: https://docs.hatched.live/docs/ai-assistants If you build with an AI coding assistant — Cursor, Claude Code, GitHub Copilot, Windsurf, Codex, Devin — these docs are designed to feed it cleanly. This page covers the three things worth wiring up. ## 1. Drop an instruction file into your repo The single highest-leverage move: give your assistant a short, opinionated brief on how to use Hatched. We maintain one for you: ```bash curl -fsSL https://docs.hatched.live/AGENTS.md -o AGENTS.md ``` [View it here](https://docs.hatched.live/AGENTS.md). It works as a standalone `AGENTS.md` (read by Codex, Cursor, Copilot's coding agent, Devin, Jules, and more), or paste its contents into `CLAUDE.md`, `.cursor/rules`, or your Copilot instructions file. It covers: - installing `@hatched/sdk-js` and initialising the client - **the cardinal rule** — secret keys (`hatch_live_*`, `hatch_test_*`) are server-only; the SDK throws in the browser. Use a widget session token or a `hatch_pk_*` publishable key client-side. - the **first-user bootstrap** — publish a config first, reuse-or-create a buddy (never re-create an egg on every load), persist `buddy_id`, mint a widget session token; the `snake_case` raw API vs camelCase SDK - the core flows: `events.send` with a stable `eventId`, `buddies.earn` / `spend` / `equip`, gates, webhooks - verifying webhook signatures against the **raw body** before parsing - catching typed `HatchedError` subclasses by `.code` - embedding widgets with the loader script (`data-session-token`) and `data-hatched-mount` elements ## 2. Point it at the machine-readable docs | File | What it is | When to use it | | --- | --- | --- | | [`/llms.txt`](https://docs.hatched.live/llms.txt) | A short index of every page — title, one-line summary, URL. Follows the [llms.txt](https://llmstxt.org) convention. | Let the assistant pick which page it needs. | | [`/llms-full.txt`](https://docs.hatched.live/llms-full.txt) | Every page concatenated into one plain-text document. | Give the assistant the whole docs in a single fetch. | | `/llm/` | Any single page as raw Markdown — e.g. [`/llm/guides/getting-started`](https://docs.hatched.live/llm/guides/getting-started). | Hand it exactly one page. | Every docs page also has **Copy as Markdown** and **Open in ChatGPT / Claude** buttons under the title, so you can ship a page to an assistant in one click. ## 3. Know the rules it must not break If you take nothing else from this page, make sure your assistant respects these — they are the mistakes we see most often: - **Secret keys never leave the server.** Not in a bundle, not in a `NEXT_PUBLIC_*` var, not in a mobile app. The SDK enforces this by throwing in DOM environments. Browser code gets a [widget session token](/docs/concepts/auth-model) (`hatched.widgetSessions.create(...)`) or a publishable key. - **Bootstrap a buddy before minting a widget token.** You can't go from `userId` straight to a session token — there's a publish → reuse-or-create egg → hatch → `buddy_id` chain, and the assistant must reuse an existing buddy rather than create an egg on every render. See [First user bootstrap](/docs/guides/first-user-bootstrap). - **`events.send` is idempotent on `eventId`.** Always pass a stable, meaningful id (`lesson_42:user_7`). Omitting it — or generating a random one on retry — double-counts. See [Sending events](/docs/guides/send-events). - **Verify webhook signatures before parsing.** HMAC over the raw bytes, then `JSON.parse`. See [Handling webhooks](/docs/guides/handle-webhooks). - **Wait on operations, don't poll.** Image-producing calls (hatch, evolve, equip) return an `operationId`; use `hatched.operations.wait(operationId)`. - **Use the SDK, not hand-rolled HTTP.** The [SDK reference](/docs/reference/sdk-js) is generated from the package source, so it never drifts. --- # Overview > The fourteen primitives that make up every Hatched gamification programme, and how they fit together. Source: https://docs.hatched.live/docs/concepts/overview Hatched gives a product a **buddy** — a companion that grows as its user does. Everything else on Hatched exists to feed the buddy: events go in, effects come out, a widget shows the result. ## The whole picture in one paragraph A user does something in your product (`lesson_completed`, `checkout_succeeded`). You send it as an **event**. The **rule engine** turns it into **effects** — skills level up, coins are earned, a badge might unlock, a streak ticks, the buddy moves closer to its next **evolution** stage. Users spend coins in the **marketplace**. Your backend gets **webhooks** when anything interesting happens. ## The primitives - **[Buddy & hatch](/docs/concepts/buddy-and-hatch)** — the avatar, born from an egg, growing through evolution stages. - **[Skills](/docs/concepts/skills)** — numeric dimensions like Grammar or Stamina that level up over time. - **[Coins](/docs/concepts/coins)** — the primary currency, earned from events, spent in the marketplace. - **[Tokens](/docs/concepts/tokens)** — secondary currencies (gems, hearts, stars) for more specialised economies. - **[Streaks](/docs/concepts/streaks)** — consistency counters that tick daily and reward habit. - **[Badges](/docs/concepts/badges)** — one-shot achievements that mark a specific moment. - **[Paths](/docs/concepts/paths)** — multi-step guided journeys (onboarding, learning modules, certification tracks) with steps and sub-steps that complete via events or manual marks. - **[Marketplace](/docs/concepts/marketplace)** — where users spend coins on items that show up in the widget. - **[Leaderboard](/docs/concepts/leaderboard)** — competitive ranking with scoped visibility. - **[Evolution](/docs/concepts/evolution)** — the long-horizon arc; the buddy changes appearance at milestones. - **[Audiences](/docs/concepts/audiences)** — segmenting one customer into multiple user groups with separate rules. - **[Config versions](/docs/concepts/config-versions)** — immutable snapshots of the whole rule set, pinned per buddy. - **[Rule engine](/docs/concepts/rule-engine)** — the deterministic pipeline that converts events into effects. - **[Webhooks](/docs/concepts/webhooks)** — signed HTTP callbacks that let your backend react to anything. ## Motivational layer primitives The Octalysis-scored surfaces that sit on top of the gamification primitives above. The Planner Radar scores how many of these you have active and how they balance across Yu-kai Chou's eight Core Drives. - **Mission Anchor** — the 40-character tenant-authored rim that lives on every widget chrome. The CD1 (Epic Meaning) backbone of the workspace. - **Hatch Ceremony** — the five-act ceremony every new member experiences once. Tenant-authored copy; the moment the workspace's narrative introduces itself. - **LEAGUES seasons** — six-week sprints with cohort-scoped tiers, a Closing Ceremony, a Hall of Fame, and a Returning Champion path. - **Kudos taxonomy** — tenant-defined recognition types (the assist metric). The CD5 (Social Influence) backbone. - **Founding Cohort Elitism** — the first-N / first-percent identity group with a lifetime status badge. - **Planner Radar / Hat Bar / Brain Bar / Phase Coverage** — the four admin diagnostic surfaces that score the workspace's motivational footprint live. ## Design principles - **Template-first over free-form.** Rule types are a fixed enum; we chose fewer knobs over infinite configurability. - **Publish before live.** Config changes land on a draft, then get published as a new immutable version. Existing buddies stay on their pinned version. - **Canonical state in Hatched.** Your product can keep a copy for UX, but Hatched owns the truth. - **Async by default.** Image generation, webhook delivery and other slow paths run off a queue and expose an operation you can poll. ## Where to next - [Getting started](/docs/guides/getting-started) — create a buddy, send an event, embed a widget in ten minutes. - [SDK quickstart](/docs/guides/sdk-quickstart) — the `@hatched/sdk-js` surface. - [Configure rules](/docs/guides/configure-rules) — tune the economy in the dashboard. - [Best practices](/docs/guides/best-practices) — patterns for an integration that scales. --- # Buddy & hatch > The avatar at the heart of Hatched;how it's created, what hatch means, and how it grows. Source: https://docs.hatched.live/docs/concepts/buddy-and-hatch A **buddy** is the persistent companion a user gets when they join your Hatched programme. Everything else — skills, coins, streaks, badges — hangs off the buddy. ## Egg and hatch Before a buddy exists, there is an **egg**. Creating an egg is the first write you make to the Hatched API: ```ts const egg = await hatched.eggs.create({ userId: 'user_42', }); ``` The egg is pinned to the current [config version](/docs/concepts/config-versions) on the customer (set during onboarding via `apply-preset`, or via the Dashboard). It doesn't render a visible buddy yet — it's a placeholder with pending image generation. Move the egg from `waiting` to `ready` once your own onboarding gate is complete; only ready eggs can hatch. **Hatch** is the ceremony that turns an egg into a buddy: ```ts await hatched.eggs.updateStatus(egg.eggId, 'ready'); const op = await hatched.eggs.hatch(egg.eggId); const buddy = await hatched.operations.wait(op.operationId); ``` Hatching is asynchronous because image generation takes 5–20 seconds. You get an operation id back; poll it with `operations.wait` or listen for the `buddy.hatched` [webhook](/docs/concepts/webhooks). ## One user, possibly several buddies By default there is one buddy per (customer, external_user_id) pair. When [audiences](/docs/concepts/audiences) are in play, a single user can have one buddy per audience — useful when the same person plays two roles (student and teacher, for instance). ## What a buddy carries A buddy is not just a picture. It holds: - a **config_version_id** — which rulebook it lives under - a **skills** map — each skill with value and level - **coins** and any **token** balances - a list of awarded **badges** - a set of **streak** counters - an **evolution stage**, current `image_url`, and bare `base_image_url` - equipped **marketplace items** - a **progression** summary for XP, badges, items, and streak counters - an **appearance** block for pending or failed item compositing - an audience tag (if configured) The widget reads this shape directly; your backend can mirror it via webhooks if you need your own source of truth for UX. ## Appearance state `buddy.appearance` separates the desired outfit from the image that is currently safe to display: - `ready` — `image_url` includes the rendered equipped items. - `pending` — a new composite is being generated. - `awaiting_credits` — the composite will retry after image credits are available. - `failed` — the render needs operator action or a retry. Use `desired_equipped_item_ids` to know what the user wants equipped and `rendered_equipped_item_ids` to know what is visible in `image_url`. If a failed appearance has `error.code === 'needs_rerender'`, regenerate the bare stage with `buddies.rerenderAppearance(buddyId)`, wait for `ready`, then re-equip the items. ## Lifecycle Buddies are long-lived. They don't expire on their own — they evolve. When you update the rule set, existing buddies stay pinned to their old config version; you migrate them explicitly when you're ready. ## Related - [Evolution](/docs/concepts/evolution) — how the buddy changes over time. - [Compositing & stages](/docs/concepts/compositing-and-stages) — the full `appearance` state machine. - [Config versions](/docs/concepts/config-versions) — the rulebook a buddy is pinned to. - [Getting started](/docs/guides/getting-started) — create and hatch your first buddy. --- # Skills > The dimensions along which a buddy grows — Pronunciation, Grammar, Stamina, whatever matters for your product. Source: https://docs.hatched.live/docs/concepts/skills Skills describe **who the buddy is**. Each skill has a numeric value, a level, an icon, and a max. They let you visualise which dimensions a learner is progressing on. ## Why skills exist Skills are the spine of gamification. They feed [evolution](/docs/concepts/evolution), act as conditions for [badges](/docs/concepts/badges), and create the "look what I learned" moment in the widget. ## How they behave A skill has: - **name** (e.g. "Pronunciation", "Grammar") - **value** — numeric, typically 0–100 or 0–1000 - **level** — derived from value, one level per N points - **icon** and **color** — visual identity in the widget ## Example > "Pronunciation" ranges 0–100, one level every 20 points. When a > `lesson_completed` event fires with `difficulty: "speaking"`, Pronunciation > gains +5. When the widget renders, it shows the current level and > progress toward the next. ## How to set them up 1. Create a skill set (e.g. "Language mastery"). 2. For each skill pick a name, icon, colour, and max level. 3. Add a [skill rule](/docs/concepts/rule-engine) — which event increments which skill, and by how much. ## Gotchas - Renaming a skill updates the widget in real time, but historical events keep the old label in the event log. - More than 8 skills crowds the widget. Don't split dimensions unless the signal is real. ## Pairing skills with decay By default skills only go up. If you want users who hit the cap to keep returning, pair the skill with a [decay rule](/docs/concepts/skill-decay) that subtracts a small amount on a schedule. Decay is opt-in per customer and configured next to skill rules in the dashboard. ## Related - [Skill decay](/docs/concepts/skill-decay) — make skills go down on a schedule. - [Rule engine](/docs/concepts/rule-engine) — how skill rules turn events into increments. - [Evolution](/docs/concepts/evolution) — skills are a common evolution trigger. - [Configure rules](/docs/guides/configure-rules) — defining skill sets and rules. --- # Skill decay > Subtract skill points on a schedule so users who go inactive feel the loss — a loss-aversion engagement loop for power users who already capped out. Source: https://docs.hatched.live/docs/concepts/skill-decay Skill decay is a **time-based** counterpart to [skill rules](/docs/concepts/skills). Skill rules add points when an event happens; decay rules subtract points when time passes. The product goal is the same engagement loop — *"come back or you'll lose progress"* — adapted for users who have already hit a cap and no longer find the next +5 motivating. ## When to turn it on Decay earns its keep when: - Power users have **plateaued** (large cohort sitting near the skill max). - Your activation curve has a clear **return-or-lose** moment (language apps, fitness streaks, certification refreshers). - You can be confident the daily/weekly ritual is **achievable** — punishing a user who never had a fair chance to log in burns trust. It's a bad fit for skills users only touch once (e.g. an onboarding score) or for cohorts where re-engagement is impossible (one-shot certifications, finite curricula). Configure rules conservatively and **watch your churn metrics for two weeks** after enabling. ## How it works A decay rule says: *"every `cadence`, subtract `amount` from `skill_key`, but never below `floor`, never if the buddy is younger than `grace_days` days, and (optionally) only if the current level is above `apply_only_above`."* | Field | What it does | | --- | --- | | `skill_key` | Which skill loses points | | `cadence` | `daily`, `weekly`, or `monthly` | | `amount` | Points subtracted each cadence period | | `floor_level` | Lower bound — decay never goes below this | | `grace_days` | New buddies are exempt for N days after creation | | `apply_only_above` | Optional. Only decay above this level | | `audience` | Scope to a single audience (default: `default`) | | `active` | Toggle without losing the configured rule | ## Lifecycle 1. **Author** the rule in **Skills → Skill Decay** (active = off). 2. **Preview** the projected curve next to each cadence option — the dashboard shows where a fresh-cap user lands in 30 days. 3. **Activate** the rule. Even active rules are gated behind the per-customer **Skill decay master switch** (`settings.features.decay`). 4. **Sweep** runs daily at **03:00 UTC**. Each (rule, buddy, period) tuple is recorded in `skill_decay_applications` so a re-run inside the same calendar period is a no-op. 5. **Webhook** `skill.decayed` fires for every buddy that lost points. `skill.updated` also fires (same shape as a rule-engine update) so downstream systems don't have to special-case the source. You can also click **Run sweep now** from the dashboard for QA — it enqueues a one-off sweep scoped to the current customer. ## Idempotency Decay is keyed by a calendar **period**: | Cadence | Period key example | | --- | --- | | `daily` | `2026-05-06` | | `weekly` | `2026-W19` (ISO 8601) | | `monthly` | `2026-05` | The unique `(rule_id, buddy_id, period_key)` row in `skill_decay_applications` makes the sweep safe to re-run inside the same period. A buddy that already lost their daily 2 points won't lose another 2 if you click *Run sweep now*. ## What it does **not** do - It does not retroactively apply for periods missed during downtime. If the worker was offline for three days, three days are skipped — decay catches up at the next sweep, once. - It does not touch coins, tokens, or badges. It is strictly skill-level. - It does not take per-customer time zones into account. The sweep uses UTC calendar boundaries (a small fidelity gap that buys a much simpler operational footprint — DST and per-tenant cron windows are not a concern at this scale). ## Related - [Skills](/docs/concepts/skills) — the underlying scalar - [Rule engine](/docs/concepts/rule-engine) — event-driven counterpart - [Webhook payloads](/docs/reference/webhook-payloads#skilldecayed) — the `skill.decayed` event shape --- # Coins > The primary currency — earned from events, spent in the marketplace, tuned with coin rules. Source: https://docs.hatched.live/docs/concepts/coins Coins are the buddy's currency. They're earned automatically from events you send, according to the **coin rules** you configure. Users spend them in the [marketplace](/docs/concepts/marketplace). ## What a coin rule is Each coin rule binds an event type (`lesson_completed`, `daily_login`) to an amount. Optional caps and multipliers shape the economy: - **Daily cap** — maximum coins of this type earned per UTC day. - **Weekly cap** — same, weekly. - **Multiplier** — for streak holders or premium users. - **Total limit** — one-shot; useful for "first-10-lessons" bonuses. ## Example ``` lesson_completed → +10 coins (daily cap: 50) streak_7 → +100 coins (one-time) checkout_complete → +500 coins (premium audience only) ``` ## How to set it up 1. List the events you already fire in your product that deserve a reward. 2. Attach an amount and optional daily cap to each. 3. Add streaks or multipliers to reward long-term play. 4. Watch the Economy Health page on the dashboard. ## Gotchas - Without daily caps, coin supply inflates fast. A user grinding 200 lessons on day one should not break your marketplace. - Balance coin earnings against marketplace prices. If nothing costs more than a week's earnings, coins become invisible. - Coins always emit `coins.earned` webhooks (and `coins.spent` on the paired spend) — subscribe if you need to mirror the ledger into your own system. ## Related - [Marketplace](/docs/concepts/marketplace) — where coins get spent. - [Tokens](/docs/concepts/tokens) — secondary currencies alongside coins. - [Configure rules](/docs/guides/configure-rules) — tuning coin rules and caps. - [Best practices](/docs/guides/best-practices) — designing an economy that doesn't reward grinding. --- # Tokens > Two-tier token model — one primary spendable, one progression accumulator. Customers pick the names. Source: https://docs.hatched.live/docs/concepts/tokens Tokens are the currencies attached to a buddy. Hatched ships a **two-tier model**: each customer configures exactly two token slots. - **Primary** — the spendable currency. Marketplace purchases, gate unlocks, and `buddies.spend` draw from this slot. - **Progression** — earn-only. Never spent; it accumulates and gates things like evolution readiness. The token names are yours. Fantasy buddies use `gems` + `mana`. A fitness app might use `reps` + `streaks`. Pick whatever fits the product. ## Why two tiers A single wallet collapses two different motivations into one number: "what can I buy" and "how far have I come". Splitting them makes both feelings legible — you can spend your gems without feeling like your overall progress regressed. Progression is deliberately **unspendable**. Attempting `buddies.spend(buddyId, { token: '' })` throws `progression_not_spendable`. That keeps long-term progress monotonic while leaving the primary slot free for economy design. ## Example ``` gems (primary) — earned from lesson_completed, spent on items + gates. Capped at 500 per week. mana (progression) — earned from quiz_passed, feeds "stage 3 at mana ≥ 1000" evolution readiness. Cannot be spent. ``` ## How to set it up 1. In the dashboard **Tokens** page, pick a `token_key` and `label` for each slot. Use `snake_case` for keys (`gems`, `xp_coins`, `mana`). 2. Optionally set an `icon` and a `max_balance` (primary only). 3. Attach earn rules per slot. Same shape as the rule engine elsewhere — event type, amount, optional cap. 4. Reference `primary.token_key` in marketplace item prices and gate costs. If you skip this step, the [onboarding plan](/docs/concepts/overview) seeds both slots from a theme catalog — a fantasy buddy defaults to `gems` + `mana`, fitness to `reps` + `streaks`, and so on. See [Token economy](/docs/concepts/token-economy) for the fallback rules. ## At runtime ```ts const summary = await hatched.buddies.tokens(buddyId); // { // primary: { key: 'gems', label: 'Gems', balance: 120, lifetimeEarned: 340, lifetimeSpent: 220 }, // progression: { key: 'mana', label: 'Mana', balance: 480, lifetimeEarned: 480, lifetimeSpent: 0 }, // } ``` Spend against the primary by default — no `token` arg needed: ```ts await hatched.buddies.spend(buddyId, { amount: 50, reason: 'gate:advanced_mode' }); ``` Pass `token` only when you have a multi-primary setup (not supported today — reserved for a future expansion). ## Surfacing it to end users Drop the [tokens widget](/docs/reference/widgets/tokens) — `
` — to show the wallet in your product: the primary balance up top, then the progression token with a progress bar toward its `max_balance`. It needs the `tokens` capability and a `read` scope; the primary number stays live off the shared `/widget/state` poll. ## Gotchas - **Progression is monotonic.** Rule engine writes go through but spend attempts are rejected. If you want a "spend XP" mechanic, that currency belongs in the primary slot, not progression. - **Token keys are immutable.** Once rules and ledger rows reference a key, renaming it in the dashboard doesn't rewrite history. Pick the key you can live with. - **Tokens emit ledger webhooks.** `token.earned` and `token.spent` fire per slot; subscribe if you sync balances downstream. - **The legacy 4-tuple is gone.** Pre-0.3 customers used `hatch_token`/`evolution_token`/`reroll_token`/`gift_token`. Migration 024 introduced the `kind` column; existing configs default to `primary`. ## Related - [Token economy](/docs/concepts/token-economy) — how the two tiers, gates, and pricing fit together. - [Coins](/docs/concepts/coins) — the default primary currency. - [Unlock gates](/docs/guides/unlock-gates) — spending the primary token to unlock features. - [Tokens widget](/docs/reference/widgets/tokens) — showing the wallet. --- # Token economy > How the two-tier token model, gates, and marketplace pricing fit together. Source: https://docs.hatched.live/docs/concepts/token-economy This page is the mental model for the whole token surface — how the two slots get seeded, where they're spent, and where they accumulate. ## The loop ``` ┌────────┐ events.send() ┌────────────┐ │ Event ├────────────────────▶│ Rule │ └────────┘ │ engine │ └─────┬──────┘ earn rules │ readiness conditions ┌──────────────────┼──────────────────┐ ▼ ▼ ┌──────────────┐ ┌──────────────────┐ │ primary │◀─── buddies.spend │ progression │ │ (spendable) │ marketplace │ (earn-only) │ │ │ gates.unlock │ evolution gate │ └──────────────┘ └──────────────────┘ ``` Events feed both slots via the rule engine. The primary slot is where users *consume*; the progression slot is where the buddy *grows*. ## How slots are seeded When you apply an onboarding plan, Hatched looks at the `token_config` bundle: - **If the plan already has two slots** (one `primary`, one `progression`) — it's used as-is. - **If the plan is empty or partial** — Hatched picks a theme from the plan description / target sector / creature style, then seeds both slots from a catalog. The catalog matrix: | Theme | Primary (spendable) | Progression (earn-only) | |-------------|---------------------|-------------------------| | `fantasy` | `gems` | `mana` | | `fitness` | `reps` | `streaks` | | `corporate` | `points` | `xp` | | `education` | `stars` | `xp` | | `tech` | `bytes` | `commits` | | `default` | `coins` | `xp` | The resolved source lands in `customers.settings.applied_sources`: ```json { "tokens": "plan" | "fallback", "marketplace": "plan" | "fallback" | "hybrid", "theme": "fantasy", "applied_at": "2026-04-22T10:30:00Z" } ``` Check that field if you're debugging why a customer ended up with `gems`/`mana` instead of the names in their plan. ## Where each slot gets used **Primary** is drawn from by: - `hatched.buddies.spend(buddyId, { amount, reason })` — direct spend. - `hatched.gates.unlock(buddyId, gateKey)` — gate cost is always primary. - [Marketplace](/docs/concepts/marketplace) item prices. **Progression** is read by: - Evolution readiness conditions (`token. >= N`). - Any custom rule engine condition that references the balance. - Dashboards, widgets, leaderboards. Attempting `buddies.spend({ token: '' })` returns `progression_not_spendable`. ## Why not one wallet A single wallet forces an ugly trade-off: spending a coin on a hat would also lower the "how far I've come" number. Two slots lets the product feel consumerist (spend, trade) *and* progressive (never regress) at the same time. If you genuinely want a single-wallet feel, set the primary token as your only visible balance and treat progression as a backend-only metric that drives evolution. Nothing in the SDK forces both to surface in the UI. ## Migrating from the legacy 4-tuple Customers created before 0.3 had a hardcoded `hatch_token` / `evolution_token` / `reroll_token` / `gift_token` contract. Migration 024 added the `kind` column and defaulted every existing row to `primary`. On the next onboarding apply, the fallback seeds a progression slot if none exists — no manual step needed. ## Related - [Tokens](/docs/concepts/tokens) — slot contract. - [Marketplace](/docs/concepts/marketplace) — primary-priced catalog. - [Unlock gates](/docs/guides/unlock-gates) — primary-spent feature flags. - [Evolution](/docs/concepts/evolution) — progression-gated stages. --- # Streaks > Consistent-daily-activity counters that reward habit, not volume. Source: https://docs.hatched.live/docs/concepts/streaks A streak is a counter that ticks once per period — once per UTC day, once per ISO week, or once per calendar month, depending on the streak's `period` (`daily`, `weekly`, or `monthly`). If the user misses a period, the counter burns. At configured milestones, a streak awards bonus coins, tokens, or badges. ## Why streaks exist Coins reward each action; streaks reward **showing up**. A well-placed streak is often the single biggest retention lever in a gamification economy. ## How they work - Bound to one event type (the "today's activity" signal). - One tick per period maximum — per UTC day for `daily`, per ISO week for `weekly`, per calendar month for `monthly`. - Milestones are tenant-configured and default to none (an empty list), so no `streak.milestone` webhook fires until you set milestones in Dashboard → Streaks. - Missing a period resets to zero unless a grace token is available. ## Example > **daily_lesson streak** — +5 coins every tick day, +50 bonus at 7 days, > +200 + 1 premium token at 30 days. Awards a "Week Warrior" badge at 7 > days that shows off in the widget. ## How to set it up 1. Pick the event type that counts as "today's activity". 2. Set a per-day bonus (optional). 3. Add milestone bonuses for the thresholds that matter to you. 4. Copy the streak `key` from Dashboard → Streaks. 5. Pick a display mode — `count`, `row`, or `mini` (a bare inline `🔥 N` chip for navbars and menus). 6. Mount the [streak widget](/docs/reference/widgets/streak) with `data-streak-key="…"` (and optionally `data-display-mode="mini"`). The streak `key` is the stable workspace-level identifier for one streak definition, such as `daily_lesson`. It is not generated by the widget and it does not come from the user's buddy state. Treat it like app configuration: hardcode it in a shared constant, store it in an environment variable such as `NEXT_PUBLIC_HATCHED_STREAK_KEY`, or pass different keys to different streak mounts when you intentionally show multiple counters. ## Gotchas - Streaks tick in UTC. Multi-timezone products may see users "burn" during their night. If this matters, model your own tick event instead of using a daily_login heuristic. - No grace days are built in — if you need them, grant a freeze token the user can burn. ## Related - [Badges](/docs/concepts/badges) — milestone streaks usually award one. - [Leaderboard](/docs/concepts/leaderboard) — rank by streaks completed. - [Streak widget](/docs/reference/widgets/streak) — rendering a streak with `data-streak-key`. - [Configure rules](/docs/guides/configure-rules) — picking the event and milestones. --- # Badges > One-shot rewards that mark a specific achievement — the "you did it" moment. Source: https://docs.hatched.live/docs/concepts/badges Where [skills](/docs/concepts/skills) draw a curve and [coins](/docs/concepts/coins) drip over time, a badge **freezes a moment**. "7-day streak". "First 10 lessons". "Helped 5 classmates". ## What badges can key off - **Milestone events** — "complete your 100th lesson" - **Streaks** — "7-day streak" - **Skill levels** — "reach Pronunciation level 5" - **Collections** — "earn all sports-themed items" - **Evolution stages** — "reach stage 3" - **Coin thresholds** — "earn 1,000 coins" - **Custom** — any combination via the rule engine ## Auto vs manual award Badges can be **auto-awarded** (the rule engine evaluates the condition and awards immediately) or **manual** (the dashboard operator or an external workflow awards them after review). Manual badges emit `badge.ready` webhooks — subscribe a moderation queue if you want humans in the loop. ## Example > **Week Warrior** — auto-awarded on 7-day streak, plays a shine animation > in the widget, fires `badge.awarded`. Also grants +50 coins as a side > effect. ## How to set it up 1. Name, icon, and description. 2. Pick a condition type (milestone, streak, skill, coin, custom). 3. Decide auto vs. manual award. 4. Optionally attach a coin or token reward. ## Gotchas - Custom conditions read the event payload — renaming properties silently breaks the rule. Version your event schemas. - Manual badges fire `badge.ready`, not `badge.awarded`. Make sure your workflow calls `POST /buddies/:buddy_id/badges` with `{ badge_key }` in the body (or `hatched.buddies.awardBadge(...)`) when the human says yes. ## Related - [Streaks](/docs/concepts/streaks) — the most common badge trigger. - [Skills](/docs/concepts/skills) and [Coins](/docs/concepts/coins) — other things badges can key off. - [Configure rules](/docs/guides/configure-rules) — defining badge conditions. --- # Paths > Multi-step guided journeys — onboarding, learning modules, activation checklists, certification tracks. Source: https://docs.hatched.live/docs/concepts/paths A path is an ordered journey made of **steps** and **sub-steps**. Each step unlocks once the previous one is complete; sub-steps inside a step finish either automatically (rule-engine event) or manually (widget CTA / API). Operators define one path per audience as the active default; users see it through the [path widget](/docs/reference/widgets/path). ## Why paths exist Coins, streaks, and badges reward isolated moments. Paths reward **progress through a defined arc** — first-value onboarding, a five-lesson unit, a seller activation checklist, a 30-day training plan, a certification track. Instead of asking users to infer "what's next," the widget shows the next concrete action and rewards completion in context. ## How they work A path has three layers: - **Definition** — label, audience, display mode, accent color, icon. - **Steps** — ordered milestones. Each step has a label, optional description, optional `unlock_condition` override, optional `completion_condition`, and reward (coins + badge). - **Sub-steps** — the actual work inside a step. Each sub-step finishes via a `completion_condition` (auto) or `allow_manual_complete=true` (manual CTA / public API). Optional `content_url` deep-links to the customer's app. Default unlock semantics: step `n+1` unlocks when step `n` is fully completed. Override per step with `unlock_condition` (e.g. "unlocks when the user earns the `novice` badge") if you need non-linear gating. Default sub-step semantics: `allow_manual_complete=true` with no `completion_condition`. The widget shows a "Mark complete" CTA (if the session token has `events:track` scope), and a `content_url` opens in a new tab. Add an automation later if you want the rule engine to mark the sub-step done off an event. Step-level completion is optional. Add a step `completion_condition` when one event should finish the whole milestone, such as `event_count(module_finished) >= 1`. When it matches, Hatched marks every unfinished sub-step in that step as complete in the same event tick, applies each sub-step reward once, then applies the step reward. Leave the step condition blank when each sub-step should complete from its own event or manual CTA. ## Display modes - **Straight column** (`display_mode: 'straight'`) — a readable vertical path with a continuous accent connector. Best for long ordered journeys where clarity matters. - **Zigzag quest** (`display_mode: 'zigzag'`) — alternating nodes and labels, Duolingo-style. Best when the path should feel more playful. - **Compact stepper** (`display_mode: 'stepper'`) — horizontal scrolling chips with the active step expanded inline. Best for short checklists, onboarding flows, or any place vertical real estate is tight. The runtime renders the same data either way — toggle freely; users see the change on next mount. ## Path icons The definition `icon` is optional decoration next to the path label. Allowed values are `path`, `flame`, `heart`, `bolt`, `star`, and `leaf`. Use `path` for no icon. Existing unsupported icon values are normalized to `path` by the path icon migration. ## Single active path per audience Activating a path atomically deactivates every other path in the same audience. There is one live path per audience at any time. Drafts stay inactive until you flip the toggle from the dashboard. ## Example > **buyer_onboarding path (audience: `seller`)** — three steps: > 1. *Set up your storefront* — sub-step "Add your first product" (manual, > deep-links to `/products/new`). > 2. *Stock your shop* — sub-step "Add five products" (auto, completes > when `event_count(product_added) ≥ 5`). > 3. *Go live* — step completion condition > `event_count(storefront_submitted) ≥ 1`, then awards > `verified_seller` badge. ## How to set it up 1. Dashboard → Paths → **New path**. 2. Pick the audience (multi-audience customers only). 3. Add steps in order; expand each one to add sub-steps. 4. Default sub-step is manual — leave it that way unless you want automation. If one event should complete the whole step, add a step completion rule instead. 5. Live preview shows fresh / in-progress / completed states. 6. **Set active** when you're ready. Activating swaps in this path and deactivates whatever was previously live for this audience. 7. Embed the [path widget](/docs/reference/widgets/path) on the customer surface — the runtime resolves the active path automatically. ## Gotchas - Audience is **immutable** after a path is created. Pick carefully on creation; if you need a different audience, clone the path. - Manual completion needs a session token with `events:track` scope. Embed-token mounts (read-only) won't show the "Mark complete" CTA. - Step completion rules cascade unfinished sub-steps and their rewards. Use them for true milestone events; use per-sub-step conditions when each task should award independently. - Add at least one sub-step when using a step completion rule. The rule persists progress by marking unfinished sub-steps complete; an empty step has no completion record to update. - A sub-step must keep either a `completion_condition` *or* `allow_manual_complete=true`. The API rejects sub-steps that drop both. - Path definition icons are curated keys, not arbitrary emoji or image URLs. Use one of `path`, `flame`, `heart`, `bolt`, `star`, or `leaf`. - Per-buddy completion state is bound to the path definition. Deleting a path removes its completion records — there is no recovery. ## Related - [Badges](/docs/concepts/badges) — step rewards are usually a badge. - [Streaks](/docs/concepts/streaks) — habit loops that run alongside a path. - [Send events](/docs/guides/send-events) — events drive automatic sub-step completion. - [Path widget](/docs/reference/widgets/path) — rendering the active path. --- # Marketplace > Where users spend coins to dress up the buddy — items, costumes, backgrounds, boosters. Source: https://docs.hatched.live/docs/concepts/marketplace The marketplace is the **consumption side** of the coin economy. Without it, coins become an empty metric. It's also where users feel "this buddy is mine". ## Anatomy of an item Each item carries: - **name**, **description**, **image** - **category** (`background`, `body`, `feet`, `hand`, `neck`, `face`, `head`, `accessory`) - **price** — in coins, tokens, or mixed - **rarity** (common, uncommon, rare, epic, legendary) - **visibility rule** — "stage 2+", "holds badge X", "audience = premium" - **equip slot** — the category also drives compositing order and conflicts ## Example > **"Cowboy Hat"** — 50 coins, rare, unlocked for Stage 2+ buddies. Once > equipped, the widget renders the buddy wearing it. ## Equip lifecycle Equipping is an appearance update, not just a metadata flip. Hatched validates ownership, enforces [category bounds](/docs/concepts/compositing-and-stages), sets the desired `equipped_items`, and then renders a new image against the buddy's `base_image_url`. The response can be instant when a cached composite exists. Otherwise it returns an operation id and the buddy reports `appearance.status: 'pending'` until the render lands. If the image provider is out of credits the status becomes `awaiting_credits` and Hatched retries after credits are available. If the base image must be regenerated, the status is `failed` with `error.code: 'needs_rerender'`; call `buddies.rerenderAppearance(...)`, wait for `ready`, then re-equip. ## How to set it up 1. Create a marketplace (pricing mode: coin / free / mixed). 2. For each item, set an image, price, and unlock type. 3. Add visibility rules (Stage 2+, holds badge X, etc.). 4. Mount the [marketplace widget](/docs/reference/widgets/marketplace) on the page where users shop. ## Gotchas - Oversized images slow the marketplace widget — cap thumbnails at 512×512. - Tune prices against the coin economy. An unsold item is usually invisible, not expensive. - A buddy can equip at most 4 items. Non-`accessory` categories are exclusive, so two `head` items conflict. - Equipped items persist across evolution stages. When a stage changes, Hatched renders the same desired item set against the new `base_image_url` and exposes any delayed composite through `buddy.appearance`. ## Related - [Coins](/docs/concepts/coins) — what items are priced in. - [Compositing & stages](/docs/concepts/compositing-and-stages) — equip slots, layer order, and the `appearance` state machine. - [Customize buddy](/docs/guides/customize-buddy) — walking an equip flow. - [Marketplace widget](/docs/reference/widgets/marketplace) — the shopping surface. --- # Compositing & stages > How equipped items layer onto the buddy, and how evolution preserves them atomically. Source: https://docs.hatched.live/docs/concepts/compositing-and-stages When a user equips a hat on a stage-1 egg and then evolves to stage 2, the hat does not disappear. This page is how that invariant is maintained end-to-end. ## The canonical categories Every marketplace item belongs to one of nine categories — eight visual compositing slots, plus a non-visual `booster` category: | Category | `layer_order` | Multi-equip? | | ------------ | ------------- | ------------ | | `background` | 10 | no | | `body` | 20 | no | | `feet` | 30 | no | | `hand` | 40 | no | | `neck` | 50 | no | | `face` | 60 | no | | `head` | 70 | no | | `accessory` | 80 | yes | | `booster` | 90 | no | `layer_order` is the compositing z-order (back → front). `background` paints first, `accessory` paints last. `accessory` is the only slot that accepts multiple equipped items — everything else rejects the second item in the same category with `category_conflict`. `booster` is a non-visual consumable category (`layer_order` 90) rather than a compositing slot, but it is a creatable category via the public API. ### Equip bounds - **Max 4 equipped items.** The fifth rejects with `too_many_items`. - **Non-accessory categories are exclusive.** Equipping a second `head` while one is already equipped rejects with `category_conflict`. - Items sort deterministically by `(layer_order, item_id)` before reaching the image pipeline, so two equipped items always composite in the same order. These checks happen at the API boundary, surfaced in the SDK as `TooManyItemsError` and `CategoryConflictError`. ## Stage-aware item assets Items can ship a stage-specific override via `stage_image_urls`: ```json { "image_url": "https://cdn.hatched.live/items/wizard_hat/base.png", "stage_image_urls": { "3": "https://cdn.hatched.live/items/wizard_hat/stage3.png", "5": "https://cdn.hatched.live/items/wizard_hat/stage5.png" } } ``` The compositing pipeline reads `stage_image_urls[currentStage]` and falls back to `image_url` when there's no override. Designers only have to ship overrides for the stages where the base asset would look wrong. ## The appearance state machine Anything that changes the buddy's image — hatch, equip / unequip, evolve — runs through the image pipeline asynchronously. The buddy carries an `appearance` block so you always know whether what you're showing is the final render. This is the single source of truth for "is the visual ready"; the buddy's economy state (coins, skills, stage) is already committed regardless. | `appearance.status` | Meaning | What to show / do | | --- | --- | --- | | `ready` | `image_url` is the final composite; `appearance.rendered_equipped_item_ids` matches `appearance.desired_equipped_item_ids`. | Show `image_url`. Nothing to do. | | `pending` | A job is generating or compositing. `appearance.operation_id` points at it. | Show `image_url` (the last good render) and an optional "updating…" hint. `operations.wait(operationId)` to know when it's done. | | `awaiting_credits` | The job is blocked on insufficient image credits. | Show the last good `image_url`. Surface a top-up prompt; the job resumes once credits land. See [Credits](/docs/billing/credits). | | `failed` | The job failed. Check `appearance.error.code`. | Show the last good `image_url`. If `error.code === 'needs_rerender'` (typically a migrated buddy with no usable bare-stage image), call `buddies.rerenderAppearance(buddyId)` — or the widget `POST /widget/appearance/rerender` with an `items:equip` session — wait for `ready`, then re-equip. For other error codes, retry the originating action. | Two fields make recovery deterministic: `base_image_url` is the trustworthy bare-stage image, and `appearance.desired_equipped_item_ids` is the desired set of item ids. `image_url` and `appearance.rendered_equipped_item_ids` are "what's currently on screen". A rerender regenerates the bare stage from scratch, after which you re-equip those item ids via the marketplace equip endpoint (you don't write an `equipped_items` field directly). ## Atomic evolve × equip The invariant that unlocks the whole feature: **the stage transition is committed atomically, while the item composite is tracked as appearance state.** What happens when a user with an equipped hat evolves: 1. Client calls `hatched.buddies.evolve(buddyId)` and receives an `operation_id`. 2. The evolve worker re-checks readiness, then generates the next-stage bare image and stores it as `base_image_url`. 3. If `appearance.desired_equipped_item_ids` is non-empty, the same job attempts to composite the desired items over that bare image. 4. If compositing succeeds, `buddy.image_url` becomes the rendered image, `appearance.rendered_equipped_item_ids` matches `appearance.desired_equipped_item_ids`, and `appearance.status` is `ready`. 5. If compositing is delayed or fails, the stage still advances. The buddy keeps the new bare stage image, `appearance.status` becomes `awaiting_credits` or `failed`, and `appearance.operation_id` points at the job that owns recovery. 6. Operation transitions to `completed`, and `buddy.evolved` fires on webhooks. Read `buddy.appearance` to decide whether the visual composite is also done. ```ts const op = await hatched.buddies.evolve(buddyId); const result = await hatched.operations.wait(op.operationId); // result.buddy.evolutionStage has advanced. // result.buddy.appearance?.status tells you whether item compositing is ready. ``` The split matters for recovery. `base_image_url` is the trustworthy bare stage. `image_url` is the currently displayable render. `appearance.desired_equipped_item_ids` is the desired set of item ids, while `appearance.rendered_equipped_item_ids` is what actually made it into the current image. If a migrated buddy reports `appearance.status === 'failed'` with `error.code === 'needs_rerender'`, call `buddies.rerenderAppearance(buddyId)` or the widget `POST /widget/appearance/rerender` endpoint, wait until `ready`, then re-equip the desired item ids. ## Demo path parity The demo widget (publishable-key `widget_sessions.demo`) runs through the same atomic pipeline via a mock image provider. Stage + equipped items still composite; evolution history rows are still written with `source: 'demo'`. That's why the marketing demo and production builds show identical behavior for this flow. ## Related - [Marketplace](/docs/concepts/marketplace) — where items live. - [Evolution](/docs/concepts/evolution) — stage triggers. - [Customize buddy](/docs/guides/customize-buddy) — walking through an equip + evolve flow end to end. --- # Leaderboard > Rank-based competition, scoped by time window and audience. Source: https://docs.hatched.live/docs/concepts/leaderboard A leaderboard ranks buddies by a scoring function over a window. It's optional — not every product benefits from ranking, and ranking can be stressful. When it fits, though, it's a strong pull. ## What defines a leaderboard - **Metric** — coins earned, skills-leveled, streaks completed, a custom sum. - **Window** — all-time, monthly, weekly, custom cohort. - **Scope** — global (across all buddies of the customer), per-audience, or per-group (classroom, team, etc.). - **Visibility** — public to all, visible only to the buddy's own rank ±3 entries, or admin-only. ## Example > **Weekly coin leaderboard, scoped to the `students` audience.** Resets > every Monday. Each student sees their own rank plus the five nearest > neighbours; admins see the full list. ## How it computes Leaderboards are computed on demand and served from a read-through cache with a 5-minute (300s) TTL. The first read after the cache expires runs the live aggregate and stores the result; subsequent reads within the window hit the cache, not the live query — this keeps performance stable regardless of scale. ## Gotchas - Leaderboards can demotivate the bottom ranks. Consider scoping visibility to "within ±N of me" rather than the whole list. - Per-group leaderboards require you to pass a `groupId` on events; without it, the event doesn't count toward group ranking. - Weekly/monthly windows reset in UTC. Cross-timezone products may see a sudden "reset" mid-day for some users. ## Related - [Audiences](/docs/concepts/audiences) — scope a leaderboard to one user group. - [Streaks](/docs/concepts/streaks) — a common leaderboard metric. - [Leaderboard widget](/docs/reference/widgets/leaderboard) — rendering it in your product. --- # Evolution > Stages the buddy grows through over time — the Tamagotchi arc. Source: https://docs.hatched.live/docs/concepts/evolution Evolution stages (between 1 and 5 — 5 is the hard maximum) trigger when conditions are met: total XP, specific skill levels, badges earned, coin thresholds. Each stage gets a new look. ## Why evolution exists Evolution is the **long-horizon motivator**. Coins reward today. Streaks reward this week. Evolution is the story that spans weeks or months: "Hatchling → Fledgling → Juvenile → Adult → Elder". (The egg belongs to the hatch ceremony — evolution stages are post-hatch, so stage 1 is the just-hatched Hatchling, not an egg.) The image pipeline regenerates the buddy's bare art at each stage. If the buddy has marketplace items equipped, Hatched then renders those items over the new `base_image_url` and reports that composite through `buddy.appearance`. ## Example > **Stage 2 unlocks at total XP ≥ 500 + "Streak 7" badge.** When met, > `evolution.ready` fires. If auto-evolve is off, your backend calls > `buddies.evolve(buddyId)` and `operations.wait` returns the new buddy > stage in 5–20s. Check `buddy.appearance.status` to confirm whether any > equipped items have finished rendering on the new stage. ## Runtime loop Event ingestion reports readiness, but the stage transition is a separate operation unless the customer's config has `auto_evolve` enabled: ```ts const effects = await hatched.events.send({ eventId, userId, type: 'lesson_completed', }); if (effects.evolutionReady) { const op = await hatched.buddies.evolve(buddyId); await hatched.operations.wait(op.operationId); } ``` With `auto_evolve: true`, Hatched starts that operation when readiness is detected and still emits the `evolution.ready` webhook for observability. ## Appearance after evolve Evolution commits the stage transition first. Item compositing is attempted in the same operation, but a credit shortage or provider failure does not roll the stage back. Instead, the buddy response exposes: - `base_image_url` — the trustworthy bare image for the new stage. - `image_url` — the current display image. - `appearance.status` — `ready`, `pending`, `awaiting_credits`, or `failed`. - `rendered_equipped_item_ids` — the item layers currently visible in `image_url`. Widgets poll `/widget/state` and show the latest safe visual while a composite is pending. For `failed` appearances with `error.code === 'needs_rerender'`, call `buddies.rerenderAppearance(buddyId)` before re-equipping items. ## Modes - **Preset** — a fixed set of 5 sprite stages per preset. - **Generative** — AI-generated art per buddy at each stage. Slower, more unique. - **Hybrid** — preset base with a generative overlay for personalised details. ## How to set it up 1. Pick an evolution model (preset / generative / hybrid). 2. Set conditions per stage (XP, skill level, badge, coin). 3. Pick a creature style (cute, sci-fi, fantasy, minimal). 4. Decide auto vs. manual evolve. ## Gotchas - `evolution.ready` fires even if auto-evolve is off. In that mode, call `buddies.evolve(buddyId)` from your backend when you want to advance the stage. - Generative mode takes 5–20s per stage; treat it as an async operation and use `operations.wait` or the widget's built-in loading state. - Equipped marketplace items must work on all stages. Test early stage equipment against late stage art, and monitor `buddy.appearance` for delayed composites. ## Related - [Compositing & stages](/docs/concepts/compositing-and-stages) — how items survive a stage change, and the `appearance` state machine. - [Buddy & hatch](/docs/concepts/buddy-and-hatch) — where a buddy starts. - [Skills](/docs/concepts/skills) — a common evolution trigger. - [Customize buddy](/docs/guides/customize-buddy) — equip + evolve end to end. --- # Audiences > Segment one customer into multiple user groups — each with its own rules, capabilities, and leaderboards. Source: https://docs.hatched.live/docs/concepts/audiences An audience is a label (e.g. `student`, `teacher`) that propagates through every buddy, event, rule, and chart. You declare them on customer settings with a short brief the rule engine and LLM can read. ## Why audiences exist Real products have multiple roles. A teacher doesn't care about the lesson-completion streak; a student doesn't care about class-level leaderboards. Audiences keep these experiences cleanly separate **without forking your customer** into two accounts. ## Example > Declare `student` and `teacher` audiences. A rule scoped to > `audience: "student"` only fires for student buddies, even though both > student and teacher buddies live under the same API key. ## How to set it up 1. Add audiences under Settings → Audiences, with a one-paragraph brief for each. 2. Scope rules, streaks, items with the `audience` field. 3. Ingest events with the `audience` key — Hatched auto-creates per-audience buddies when needed. 4. Filter every analytics card by audience from the top-of-page dropdown. ## Gotchas - Removing an audience is blocked while any buddy still references it. The dashboard shows you the blocking buddies. - A single `externalUserId` can have **one buddy per audience** — handy for users who play both roles. - Audience briefs are read by the LLM that generates copy and images. Short, direct briefs produce better results than verbose ones. ## Related - [Config versions](/docs/concepts/config-versions) — audience-scoped rules live in the same versioned snapshot. - [Leaderboard](/docs/concepts/leaderboard) — scope rankings per audience. - [Configure rules](/docs/guides/configure-rules) — adding per-audience overrides. --- # Config versions > Immutable snapshots of every rule, skill, coin payout, and item. Buddies pin to a snapshot so their world never shifts unexpectedly. Source: https://docs.hatched.live/docs/concepts/config-versions Drafts are your working copy. Publishing freezes them. Each buddy carries a `config_version_id` that the [rule engine](/docs/concepts/rule-engine) loads on every event. Migration is explicit — you decide when a buddy moves to a newer version. ## Why versions exist Without versioning, tweaking a coin payout silently changes every live buddy's behaviour. With it, you can always explain exactly why a buddy did what it did. ## Example > Bump `lesson_completed` from +10 → +25 coins in the draft. Publish. New > eggs hatch pinned to v12. Existing buddies stay on v11 until you migrate > them. ## How to work with drafts 1. Make changes freely — they land on the draft. 2. Review the diff under Publish before flipping it live. 3. Migrate buddies in bulk or individually when you're ready. ## Lifecycle - **Draft** — your working copy, mutable. - **Published** — immutable; new buddies pin to it. - **Archived** — old snapshot still valid for pinned buddies, no longer available for new pins. ## Gotchas - Migration swaps the **rulebook**, never the state. Coins, badges, streak counters all carry over when a buddy moves to a new version. - Archiving a version doesn't migrate its buddies; they keep running under the old snapshot until you explicitly move them. - Preset changes from Hatched itself land as a new version for your customer — they never silently modify your draft. ## Related - [Rule engine](/docs/concepts/rule-engine) — loads the pinned version on every event. - [Audiences](/docs/concepts/audiences) — per-audience rule overrides live in the version. - [Configure rules](/docs/guides/configure-rules) — editing drafts and publishing. --- # Rule engine > The deterministic two-phase pipeline that converts events into effects. Source: https://docs.hatched.live/docs/concepts/rule-engine The rule engine is the heart of Hatched. Every event you send goes through the same pipeline; every effect the buddy accumulates is the output of that pipeline. ## The two-phase contract 1. **Compute phase** — read-only. Given the current buddy state and the incoming event, the engine computes what *would* change: coin increments, skill increments, badges newly eligible, token deltas, streak ticks. 2. **Apply phase** — transactional. Opens a single database transaction, takes a `pessimistic_write` lock on the buddy row, writes every computed effect, commits atomically. If any step throws, everything rolls back. 3. **Post-transaction** — non-atomic side effects: progression counters, evolution readiness check, webhook emission. ## Why this split Two properties fall out of the contract: - **Idempotency** — the same `event_id` produces exactly one effect even if retried. - **Race-freedom** — concurrent events for the same buddy serialise on the row lock, so two overlapping lessons don't both award the same badge. ## Two ingestion paths — by design - `POST /events` — standard ingestion. The rule engine owns the decision. - `POST /buddies/{buddy_id}/coins`, `POST /buddies/{buddy_id}/coins/spend`, `POST /buddies/{buddy_id}/tokens`, `PATCH /buddies/{id}/skills`, `POST /buddies/{buddy_id}/badges` (badge to award is `badge_key` in the body) — administrative override. Every write still funnels through the same transactional services, so ledger invariants are preserved. This is intentional. Don't try to collapse them into a single endpoint. ## Unknown events Events not declared on the customer's [config version](/docs/concepts/config-versions): - Every event type — declared or not — is recorded in `buddy_progression_metrics.custom_counters` via a `jsonb_set` update, keyed by the event's own name. There are no reserved event names and no hardcoded counter columns. Downstream handlers still evaluate, and an undeclared type also emits a warning log. **Never dropped.** ## Observability Every event carries a `requestId` that follows it through the rule engine, into effect ledger entries, and out into webhooks. Keep the `requestId` from API responses and webhook payloads when contacting support; Hatched uses that value to trace a specific event through retries, ledgers, and webhook delivery without asking you to expose secret keys or raw database state. ## Related - [Config versions](/docs/concepts/config-versions) — the immutable rulebook the engine loads per buddy. - [Coins](/docs/concepts/coins), [Skills](/docs/concepts/skills), [Badges](/docs/concepts/badges) — the effects the engine produces. - [Send events](/docs/guides/send-events) — what to send, with stable `eventId`s. --- # Webhooks > Signed HTTP callbacks when something happens in the buddy's world — HMAC-signed, 3 retries, 5-minute replay window. Source: https://docs.hatched.live/docs/concepts/webhooks Point Hatched at one of your endpoints and it will `POST` JSON whenever a subscribed event fires — coin earned, badge awarded, streak milestone, buddy evolved, etc. The signature is HMAC-SHA256 over `${timestamp}.${body}` using the secret we show **once** at creation. ## Why webhooks [Widgets](/docs/reference/widgets/buddy) cover the presentation layer; webhooks cover the business layer. Unlock a course when a badge fires, notify Slack on streak milestones, sync coins into your own ledger. ## Example > Subscribe to `badge.awarded`. When "7 Day Streak" fires, your backend > grants the user a premium feature for 24h. ## How to set it up 1. Add an endpoint under Settings → Webhooks and pick event types. 2. **Store the secret** — it is shown only at creation time. Rotate it in place via the dashboard or `client.webhooks.rotateSecret(endpointId)` — the endpoint URL and event subscriptions are preserved. See the rotation playbook in [Verify webhooks](/docs/guides/verify-webhooks). 3. On receipt, **verify HMAC** + timestamp, reject replays older than 5 minutes. 4. Return 2xx quickly; non-2xx triggers retries at +5s, +30s, +5min. See [Handle webhooks](/docs/guides/handle-webhooks) for a complete verification example in Node. ## Gotchas - Sign over the **raw body bytes**, not a re-serialized JSON. JSON parsers reorder keys, which breaks the signature. - Delivery log keeps every attempt — use it when something looks off. - Webhooks are not ordered. If you need strict ordering, write to a queue on your side and sequence from there. ## Related - [Handle webhooks](/docs/guides/handle-webhooks) — signature verification, replay window, retries. - [Webhook payloads](/docs/reference/webhook-payloads) — the shape of every event type. - [Rule engine](/docs/concepts/rule-engine) — what produces the events you subscribe to. --- # Webhook delivery > How Hatched enqueues, signs, retries, and dedupes webhook deliveries — what the platform promises and what it expects from your handler. Source: https://docs.hatched.live/docs/concepts/webhook-delivery This page is the contract between Hatched and your backend. The [Verify webhooks guide](/docs/guides/verify-webhooks) covers the per-framework handler code; this one explains the _system_ delivering them. ## At-least-once delivery Hatched stores every webhook payload in a BullMQ queue the moment the originating event commits. Delivery is **at-least-once** — never zero, but the same `X-Hatched-Delivery` id can arrive more than once if your endpoint returns a non-2xx before our retry window expires. The implication: your handler **must be idempotent**. Hatched does not attempt server-side delivery deduplication on your behalf because the correct dedupe boundary is your business logic, not the HTTP layer. The [Idempotency](/docs/guides/verify-webhooks#idempotency-in-detail) section of the verify guide has the canonical Redis-SETNX pattern. ## Retry curve When your endpoint returns a 4xx/5xx or times out (default 10s), Hatched re-enqueues the delivery with exponential backoff: | Attempt | Delay since previous | | ----------- | -------------------- | | 1 (initial) | — | | 2 | +5 seconds | | 3 | +30 seconds | | 4 (final) | +5 minutes | After the fourth attempt the delivery is marked `failed` in the delivery log. Hatched **does not retry automatically beyond that** — the operator can replay manually from the dashboard once the endpoint is healthy. A 2xx response any time during the window stops retries. A 4xx terminates faster than a 5xx because Hatched assumes the payload is structurally unacceptable (most often: signature reject). Both still mark the delivery `failed` after the final attempt. ## Delivery id uniqueness Webhook metadata lives in HTTP headers, not in the JSON body: - **`X-Hatched-Event`** — the event name (`badge.awarded`, `buddy.hatched`). - **`X-Hatched-Delivery`** — the outbound delivery id. This is the dedupe key. The body is the raw per-event payload and does not contain a universal `deliveryId`, `eventId`, `type` or `data` envelope. Some event payloads carry domain ids such as `event_id`, `ledger_id`, `purchase_id` or `buddy_id`; use those only when you intentionally want once-per-business-object semantics. ## Producer idempotency Hatched itself dedupes on the _producer_ side using an internal idempotency key derived from the originating action. Re-running the same business action — for example a retried hatch on a stuck operation — will not emit duplicate webhooks for the parts that already succeeded. This is separate from your consumer-side dedupe and you don't need to do anything to benefit from it. ## Ordering Hatched does not guarantee global ordering. Two events for the same buddy _tend_ to arrive in send order because the queue is FIFO per partition, but cross-buddy or cross-event ordering is not reliable. If ordering matters for your business logic, carry or compare domain-specific timestamps/sequence ids in the payload rather than relying on arrival order. ## Replay window Each delivery carries an `X-Hatched-Timestamp` header (unix seconds) that Hatched signs alongside the body — the `X-Hatched-Signature` HMAC is computed over `` `${timestamp}.${rawBody}` ``. SDK adapters reject anything older than 300 seconds by default — same convention as Stripe / Slack / GitHub. Consequences: - Every (re)delivery is re-signed with a fresh `X-Hatched-Timestamp` at send time, so there is no server-side age check — a retry minutes later still carries a current timestamp, and dashboard replays carry a fresh, valid one too. The 5-minute window is enforced **only on your side** by the verifier (SDK adapters default to a 300s tolerance). - You **must** validate the timestamp on your side. The SDK adapter does this automatically; manual implementations need to compare against `Date.now() / 1000`. A persistent ~30s skew between Hatched and your handler signals NTP drift on your host — fix the clock rather than widening the tolerance. ## Delivery log Every delivery — successful or failed — is recorded in `webhook_delivery_logs` with: - The masked request URL - The signed raw payload - Response status + body excerpt - Attempt number - Duration Dashboard → Developers → Webhook deliveries surfaces this log per endpoint. The SDK exposes it via `client.webhooks.deliveries({ endpointId })`. ## Health alerts and digest emails Hatched rolls the delivery log into a customer-scoped health summary: - `GET /webhook-configs/health` is available from the Webhooks settings surface and is not gated by analytics packaging. - `GET /analytics/webhooks` returns the same contract for analytics dashboards and CSV exports. - The summary includes active endpoint count, success rate, recent failures, retries in the last 24 hours, top failing events, alert severity, recommended action, and the last digest timestamp. - A worker sweep runs hourly and sends at most one webhook health digest email per workspace per UTC day while delivery is degraded or has recent failures. The digest links back to Settings → Webhooks so an operator can inspect recent deliveries and replay the affected event after fixing the receiver. ## Dead-letter handling Failed deliveries stay in the log indefinitely (retention follows the customer's data retention setting, default 90 days). The operator can: 1. Inspect the response body to debug the handler. 2. Click **Replay** in the dashboard, or call `client.webhooks.replay(endpointId, deliveryId)`, after fixing the endpoint. 3. Bulk-replay a date range when migrating to a new endpoint. There is no separate DLQ — the delivery log _is_ the DLQ. ## Cause webhooks The `cause.threshold_reached` event uses a parallel delivery system that ships per-cause webhook URLs configured in the dashboard, rather than the customer-wide endpoints. Same signing envelope and same replay window, but a different retry curve — 3 attempts (initial + 2 retries) with +1s/+4s backoff, dispatched inline rather than via the BullMQ queue. The [webhook payloads reference](/docs/reference/webhook-payloads#cause-threshold-reached) covers the wire format. ## What this means for your handler In summary, your endpoint needs three guarantees and one habit: 1. **Idempotent** — `X-Hatched-Delivery` dedupe before any side effect. 2. **Signature-verifying** — never trust the body before checking the HMAC. 3. **Fast** — acknowledge with `2xx` within 10s; queue slow work. 4. **Observable** — log the delivery id and response status so you can correlate platform-side dashboard entries with your own traces. Get those right and the platform handles the rest. --- # Auth model > Secret keys, publishable keys, widget session tokens, and embed tokens — which one to use, when, and why. Source: https://docs.hatched.live/docs/concepts/auth-model Hatched exposes four token types. They exist because different parts of your product have different trust boundaries, and mixing them up is the single most common reason integrations get shipped with secret-key leaks. ## The four tokens | Token | Prefix | Where it lives | Can do | | ------------------------ | ------------------------------ | ---------------- | ------------------------------------------------------------- | | **Secret API key** | `hatch_live_*`, `hatch_test_*` | Server (env var) | Everything. Full account access. | | **Publishable key** | `hatch_pk_*` | Browser (safe) | Read buddies/operations, mint embed tokens, unlock token gates. | | **Widget session token** | JWT | Browser | Scoped interactive actions (track, buy, equip) for one buddy. | | **Embed token** | JWT | Browser | Read-only widget display for one buddy. | ## Session token vs embed token These two are the easiest to mix up — they're both browser JWTs scoped to one buddy, but they come from different endpoints and do different things: | | Widget session token | Embed token | | --- | --- | --- | | Minted by | `POST /api/v1/widget-sessions` — `hatched.widgetSessions.create(...)` | `POST /api/v1/embed-tokens` — `hatched.embedTokens.create(...)` | | Requires `scopes`? | **Yes** (`['read', 'events:track', ...]`) — sending none is rejected | **No** — the endpoint rejects a `scopes` field | | Loader attribute | `data-session-token` | `data-embed-token` | | Widget mode | `interactive` — can track events, purchase, equip | `read-only` — display only | | Server-side state | Tracked (revocable via `widgetSessions.revoke`) | Stateless (validity = JWT signature + `exp`) | | Default TTL | 1h (max 1h) | 24h (max 24h) | Rule of thumb: if any widget on the page needs to *do* something (track an event, buy or equip an item), mint a **session token**. If every mount is purely display, an **embed token** is cheaper. Both require an existing `buddy_id` — see [First user bootstrap](/docs/guides/first-user-bootstrap). ## Decision tree ``` Is this code running on the browser? ├── No (Node, edge, server component, route handler) │ → Secret API key (HATCHED_API_KEY env var) │ └── Yes ├── Do you need mutation (send event, earn coin)? │ → Call your own backend route with the secret key. │ Never put a secret key in the browser bundle. │ ├── Do you need the user to interact with widgets? │ → Server mints a widget session token, │ browser loads widget.js with data-session-token. │ ├── Do you only need to display widgets/read-only state? │ → Server mints an embed token (cheaper, stateless). │ └── Do you need raw API reads from a SPA/static site? → Publishable key in the browser + @hatched/sdk-js with { publishableKey }. ``` ## Secret API key ```ts import { HatchedClient } from '@hatched/sdk-js'; const hatched = new HatchedClient({ apiKey: process.env.HATCHED_API_KEY!, }); ``` **Rules.** - Load from an env variable. **Never** hard-code. - Rotate on any suspected leak. Rotation is instant — old key returns 401 immediately. - The SDK throws at construction if it detects a DOM environment (`window` and `document` are present). - `allowBrowser: true` is an escape hatch for **unit tests only**. It is honored only when the SDK detects a test runner — any of `NODE_ENV=test`, `VITEST`, `JEST_WORKER_ID`, or `expect`+`it` globals from Jest/Vitest/Jasmine/Mocha. In a real production browser the construction throws even with `allowBrowser: true`, so you cannot accidentally ship a secret key by leaving the flag on. When a test runner *is* detected the SDK emits a one-line `console.warn` so the bypass is visible in logs. - For genuinely browser-side reads, use a `publishableKey` (see below) or a widget session token minted server-side. ## Publishable key ```ts const hatched = new HatchedClient({ publishableKey: 'hatch_pk_xxxxxxxx', }); const buddy = await hatched.buddies.get(buddyId); // ✅ ok await hatched.events.send({ ... }); // ❌ PublishableKeyScopeError ``` **Rules.** - Publishable keys are **scoped** to a fixed allow-list: `read:buddies`, `read:operations`, `read:marketplace`, `write:embed-tokens`, and `write:unlocks`. Today, the explicitly publishable-allowed public API endpoints are read buddies, read operations, mint embed tokens, and unlock token gates. `read:marketplace` is reserved for future publishable marketplace reads; widget marketplace browsing uses a widget token. All other endpoints return `403 publishable_key_scope`. - Safe to commit to a browser bundle, include in `` tags, or expose as `NEXT_PUBLIC_*`. - Per-key scope is configurable in Dashboard → Developers → API keys → **Create publishable key** → check the endpoints you want to allow. ## Widget session token The flow for an interactive widget (buddy, marketplace, celebrate): 1. Browser asks your backend for a session. 2. Backend calls `hatched.widgetSessions.create(...)` with a secret key. 3. Backend returns `{ token, expiresAt }` to the browser. 4. Browser loads `widget.js` with `data-session-token` and mounts `
`. 5. The widget talks to Hatched directly, signed with the session token. ```ts const session = await hatched.widgetSessions.create({ buddyId: 'bdy_abc', userId: 'user_42', scopes: ['read', 'events:track', 'marketplace:browse', 'marketplace:purchase', 'items:equip'], ttlSeconds: 60 * 15, }); ``` **Rules.** - Short-lived (minutes, not hours). Re-mint on focus or route change. - Scoped to one `buddyId`. If you switch buddies, re-mint. - Scoped to the exact list of widget scopes you pass. A session minted without `marketplace:purchase` cannot buy items even if the widget tries. ## Embed token Read-only sibling of widget session tokens. Stateless and cheap to mint — pass one per buddy/widget render on a page. **This is the token that confuses people most often.** It is *not* something you create once in the dashboard like an API key. It is a short-lived JWT that your backend mints on demand — typically inside a route handler — and hands to the browser for the page render. ### Why it exists The widget runs in the user's browser. It needs *some* token to identify which buddy to display, but you cannot put a secret API key in the browser (any visitor could read it from devtools and call mutating endpoints with your account's full authority). The embed token solves this: it is signed by Hatched, scoped to one `(userId, buddyId)` pair, expires automatically, and can only do read-only widget display: buddy, badges, streaks, leaderboards, and marketplace catalog/state. ### How to mint one ```ts // app/api/hatched/embed-token/route.ts — Next.js import { HatchedClient } from '@hatched/sdk-js'; const hatched = new HatchedClient({ apiKey: process.env.HATCHED_API_KEY! }); export async function POST(req: Request) { const { userId, buddyId } = await req.json(); const embed = await hatched.embedTokens.create({ userId, buddyId, ttlSeconds: 60 * 60, // 1h is a reasonable default }); return Response.json({ token: embed.token, expiresAt: embed.expiresAt }); } ``` Or with raw HTTP if you do not use the SDK: ```http POST /embed-tokens Authorization: Bearer hatch_live_xxxxxxxxxxxx Content-Type: application/json { "user_id": "user_42", "buddy_id": "bdy_abc", "ttl_seconds": 3600 } ``` Response: ```json { "token": "eyJhbGciOi…", "expires_at": "2026-05-05T16:00:00Z", "mode": "read-only" } ``` ### How it reaches the browser ```html
``` ### Lifecycle - **Stateless**: Hatched does not store embed tokens. Validity comes from the JWT signature and the `exp` claim — there is no revocation list. - **TTL**: minimum 5 minutes, maximum 24 hours, default 24 hours. Pick the shortest TTL that fits your render cadence. - **Re-mint, do not cache for long**: mint on each page request (or each SPA route change). The mint call is cheap. - **Difference from a widget session token**: an embed token can only *display* — it cannot send events, equip items, or buy from the marketplace. For interactivity, use a widget session token instead. ## What lives where | Layer | Token | | ---------------------------------------------------------- | ----------------------------------- | | `.env` / Vercel secrets / GitHub secrets | Secret API key | | `NEXT_PUBLIC_HATCHED_PK` / HTML `` | Publishable key | | Request to your `/api/hatched/session` endpoint | — returns widget session token | | `data-session-token` / `data-embed-token` script attribute | Widget session token or embed token | ## What not to do - ❌ Put a secret key in a `.env.production` file that gets shipped to the browser via Vite/webpack `DefinePlugin`. Check your bundler output. - ❌ Use a session token to call the raw API from `fetch` — session tokens are only accepted by the widget runtime. - ❌ Reuse a single session token across many users — tokens are user-bound. - ❌ Assume a publishable key is "read-only enough" to skip scope review — check the scope set before publishing a new one. ## Related - [Widget integration](/docs/guides/widget-integration) - [Browser usage with publishable key](/docs/guides/browser-usage) - [Error: publishable_key_scope](/docs/reference/error-codes#publishable-key-scope) --- # Idempotency > How retry-safe mutations work in the Hatched API — the Idempotency-Key contract, what the platform caches, and how the SDK uses it automatically. Source: https://docs.hatched.live/docs/concepts/idempotency Hatched accepts an `Idempotency-Key` header on every mutating endpoint (`POST`, `PUT`, `PATCH`, `DELETE`). When you supply one, the platform guarantees that retries of the *same* request return the *same* response — body, status, and side effects — for 24 hours. This is the same shape Stripe, Slack, and AWS use. Apply it whenever a network blip, timeout, or client crash could cause you to repeat a request that should run exactly once. ## How it works 1. You send `Idempotency-Key: ` alongside the request. 2. The interceptor hashes the request method, path, and body to produce a fingerprint. 3. **Cache miss** → handler runs normally. The response (body + status) is cached under `idem:{customer_id}:{key}` for 24 hours. 4. **Cache hit, matching fingerprint** → the cached response replays. `Idempotency-Replayed: true` is set on the response so you can tell the difference in a log line. 5. **Cache hit, *different* fingerprint** → `409 idempotency_key_conflict`. You reused the key for a different request. Use a fresh key. Failed responses (`4xx` / `5xx`) are **not** cached — retries should produce a fresh attempt. Cache only sticks on `2xx` success. ## Picking a key A good key is: - **Unique per logical action.** Use the business id (`order_`, `lesson__user_`), not a clock value. - **Stable across retries.** The whole point: a retry must reuse the original key. - **Opaque to the user.** Never expose it in a URL. UUID v4 works fine when you don't have a natural business id. The SDK auto-generates one for you (next section). ## SDK behaviour `@hatched/sdk-js` injects `Idempotency-Key` automatically on mutating resource methods. Most callers never have to think about it: ```ts // Auto-generated, retried safely on transient network errors. await hatched.eggs.create({ userId: 'user_42', ensure: true }); ``` To pass an explicit key from your own business logic — recommended for operations you may retry from a different process — set it via the options object: ```ts await hatched.events.send( { eventId: 'lesson_42_user_99', userId: 'user_99', type: 'lesson_completed', properties: { … }, }, { headers: { 'Idempotency-Key': `lesson:42:user:99` }, }, ); ``` ## What this is *not* - **Not event-level deduplication.** Use `eventId` on [send events](/docs/guides/send-events#idempotency) for that — it lives in the rule-engine, not the HTTP layer. - **Not webhook idempotency.** That contract is on the consumer side — see [Webhook delivery](/docs/concepts/webhook-delivery#idempotency-in-detail). - **Not a queue.** Replays return the cached response immediately; they don't re-enqueue work. ## When to skip it Read-only requests (`GET`, `HEAD`) ignore the header — they're naturally idempotent. One-shot operations that intentionally produce a side effect each time (charging a credit grant, issuing a unique token) should reuse a *different* key per attempt rather than letting the platform replay. If you need a route that explicitly opts out of idempotency caching, flag it on your side — Hatched will keep the header behaviour but you can set the key to a unique UUID per attempt to force a fresh run. --- # Pagination > Hatched ships two pagination envelopes — cursor (canonical for new endpoints) and offset (legacy). This page documents both shapes, the SDK helpers that walk them, and how to add cursor pagination to a server-side route. Source: https://docs.hatched.live/docs/concepts/pagination import { Callout } from 'fumadocs-ui/components/callout'; Every Hatched list endpoint returns one of two envelopes. New endpoints use **cursor pagination**; older endpoints still return the offset shape. The SDK has helpers for both — application code doesn't need to care which shape it's reading. ## Cursor pagination (canonical) The canonical envelope — emitted by the server-side `cursorPaginate` helper, the shape new endpoints should adopt: ```json { "data": [ /* page rows */ ], "pagination": { "nextCursor": "eyJrIjoiMjAyNi0wNS0yNVQxODoxMjozNFoiLCJpZCI6IjkyZS4uLn0", "hasMore": true, "limit": 50 } } ``` The cursor is opaque — treat it as a JWT-ish blob. It encodes the sort key + tie-breaker id of the last row in the current page, so the next call returns the rows immediately after it. The cursor resources shipped today do **not** yet return this envelope. Each exposes the cursor at the top level on its own data key: `notifications.list` → `{ notifications, nextCursor, unreadCount, pausedUntil }`, `feed.teamEvents.list` → `{ events, next_cursor }` (snake_case), `webhooks.deliveries` → `{ data, nextCursor }`. The `{ data, pagination }` envelope is what `cursorPaginate` produces for new server routes; until a resource is wired to it, adapt the resource's real shape into the `paginateCursor` fetcher (see [Walking pages from the SDK](#walking-pages-from-the-sdk)). **Why cursor.** Stable under concurrent writes (no missed/duplicated rows when new data lands between page calls), supports unbounded streams (no upper bound from `total` reaching memory limits), and aligns with keyset indexing so latency stays flat as data grows. ### Request Pass `cursor` and `limit` query params: ``` GET /api/v1/widget/notifications?cursor=eyJrIjoiMjAy...&limit=100 ``` - `cursor` (optional) — omit on the first request; pass the previous response's `nextCursor` (the canonical envelope nests it under `pagination.nextCursor`) on subsequent requests. - `limit` (optional) — page size, clamped server-side. The `cursorPaginate` helper defaults to 50 and clamps to `[1, 200]`. ### Response invariants - `data.length` is always `≤ pagination.limit`. - `pagination.hasMore === (pagination.nextCursor !== null)`. - When `nextCursor === null`, the cursor chain is exhausted. ## Offset pagination (legacy) The envelope: ```json { "data": [ /* page rows */ ], "meta": { "total": 1284, "page": 1, "limit": 50 } } ``` **Why we still ship it.** Several endpoints already exposed this shape publicly. The contract is preserved; new endpoints just don't add to the surface. ### Request Pass `page` and `limit`: ``` GET /api/v1/buddies?page=2&limit=100 ``` - `page` (1-indexed, default 1). - `limit` (default 20, max 100). A `limit` above 100 is rejected with `422 validation_failed`. ### Response invariants - `meta.total` is the authoritative termination signal — the SDK paginator stops when `(page-1) * limit + data.length >= total`. Don't infer "this is the last page" from `data.length < limit`: filtered endpoints (status filter, soft deletes, audience scoping) routinely return a short page in the middle of the stream. - `data.length` may be `0` on the final page (when `total` is an exact multiple of `limit`). ## Walking pages from the SDK The SDK exposes two pairs of helpers. Each returns an `AsyncIterableIterator` so you can `for await … break` to stop early without fetching the next page. ### Cursor `paginateCursor` / `collectCursor` expect the canonical `{ data, pagination: { nextCursor } }` envelope. Today's cursor resources return the cursor at the top level on their own data key, so map the resource's real shape into that envelope inside the fetcher: ```ts import { paginateCursor, collectCursor } from '@hatched/sdk-js'; for await (const note of paginateCursor((cursor) => hatched.notifications.list({ cursor, limit: 100 }).then((page) => ({ data: page.notifications, pagination: { nextCursor: page.nextCursor, hasMore: page.nextCursor !== null, limit: 100 }, })), )) { if (note.id === target) break; } // or buffer everything const all = await collectCursor((cursor) => hatched.notifications.list({ cursor, limit: 100 }).then((page) => ({ data: page.notifications, pagination: { nextCursor: page.nextCursor, hasMore: page.nextCursor !== null, limit: 100 }, })), ); ``` ### Offset ```ts import { paginate, collect } from '@hatched/sdk-js'; for await (const buddy of paginate( (page) => hatched.buddies.list({ page, limit: 100, status: 'active' }), )) { console.log(buddy.id); } const active = await collect( (page) => hatched.buddies.list({ page, limit: 100, status: 'active' }), ); ``` Both pairs accept a `maxPages` runaway guard and an `AbortSignal`: ```ts const ac = new AbortController(); setTimeout(() => ac.abort(), 5_000); const bounded = await collectCursor( (cursor) => hatched.notifications.list({ cursor, signal: ac.signal }).then((page) => ({ data: page.notifications, pagination: { nextCursor: page.nextCursor, hasMore: page.nextCursor !== null, limit: 50 }, })), { signal: ac.signal, maxPages: 20 }, ); ``` ## Server-side: adding cursor pagination to a new endpoint For new endpoints in `apps/api`, use the shared cursor helper instead of writing keyset logic by hand: ```ts import { cursorPaginate } from '@/common/pagination/cursor'; @Get('items') async list(@Query() query: CursorListItemsDto) { const qb = this.itemsRepo .createQueryBuilder('item') .where('item.customer_id = :customerId', { customerId }); return cursorPaginate({ qb, sortColumn: 'item.created_at', tieBreakerColumn: 'item.id', direction: 'DESC', limit: query.limit, cursor: query.cursor, }); } ``` The helper produces the canonical envelope, applies a keyset filter so the SQL plan stays on an index, and uses the row's `createdAt` + `id` as the cursor tuple by default. Override `toCursor` for endpoints sorted by a different column. ### Choosing a sort key The sort column must be **monotonic** within the result set — usually `created_at`. The tie-breaker is always a unique id (UUID). With those two together, two rows can never tie, so the keyset predicate is exact and the cursor never drops or duplicates rows. ## When to migrate an offset endpoint to cursor Existing offset endpoints stay as-is. Migrate when one of these is true: - The list grows unbounded (events, operations, ledger entries) and `OFFSET N LIMIT M` starts to scan a problematic number of pages. - Consumers report missed/duplicated rows under concurrent writes. - You need to expose a streaming API on top of the same data. When you migrate: add the `cursor` query param alongside `page`; serve the cursor envelope when `cursor` is present, the offset envelope otherwise. Consumers pick the matching SDK helper — `paginateCursor` / `collectCursor` for the new shape, `paginate` / `collect` for legacy offset. There is no envelope autodetection on the client; using the wrong helper either misses rows or loops forever, so the two pairs are explicit by design. ## Companion docs - [Listing endpoints](/docs/reference/http-api) — every list endpoint with its current pagination shape. - [SDK reference](/docs/reference/sdk-js) — `paginate`, `paginateCursor`, `collect`, `collectCursor`. --- # Getting started > Ten minutes from zero to a buddy in your product — create an egg, send your first event, embed a widget. Source: https://docs.hatched.live/docs/guides/getting-started import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; This guide walks through the full integration path. If you only have ten minutes, this is the one to read. Every step ships TypeScript first (with `@hatched/sdk-js`) plus raw HTTP examples for backends in other languages. > Wiring this into a real app? Read [First user bootstrap](/docs/guides/first-user-bootstrap) > alongside it — same flow, with the parts you can't skip spelled out: publish > your config first, **reuse an existing buddy instead of creating a new egg on > every load**, persist `buddy_id`, the `snake_case` raw API, and hatch latency. > Skipping those is the #1 cause of broken first-run integrations. ## 1. Sign up and grab an API key 1. Create an account at the [Hatched dashboard](https://hatched.live). 2. Generate/apply a plan or pick a dashboard preset — `language-learning`, `fitness`, `productivity`, or `custom`. That step creates the event types the first event will use, such as `lesson_completed`, so the first ingest does not fail with `event_type_not_registered`. 3. **Publish your config.** Picking a preset in step 2 publishes your first config version automatically, so `eggs.create` works straight away. (If you built a config from scratch, open the rules editor and hit Publish — `eggs.create` returns `409 no_published_config` until one is published. Later edits also sit on a draft until you publish them.) New buddies pin to the snapshot you publish. 4. Go to **Developers → API keys** and create a **secret key** (prefix `hatch_live_` in production, `hatch_test_` for sandbox). 5. Keep **Developers → Verify installation** and **Settings → Event Log** open while you test. The first screen checks your widget snippet; the event log confirms the API accepted the event and shows the returned effects/debug payload. Secret keys are server-only. Never ship one to a browser bundle. ## 2. Install the SDK ```bash pnpm add @hatched/sdk-js # or npm install @hatched/sdk-js ``` ```ts import { HatchedClient } from '@hatched/sdk-js'; const hatched = new HatchedClient({ apiKey: process.env.HATCHED_API_KEY!, }); ``` > The SDK throws on construction if it detects a browser runtime. For > browser integrations, mint a widget session token server-side > (step 5) or use a [publishable key](/docs/concepts/auth-model). ## 3. Create an egg and hatch it A buddy is born from an egg. **Do this once per user** — before creating an egg, check whether the user already has a buddy (`hatched.buddies.list({ userId })`) or whether you've stored one. Creating an egg on every page load fills up the per-user egg limit; the [bootstrap guide](/docs/guides/first-user-bootstrap) has the full reuse pattern. `ensure: true` makes the create call reuse this user's existing `waiting`/`ready` egg if there is one. ```ts const egg = await hatched.eggs.create({ userId: 'user_42', ensure: true }); if (egg.status === 'waiting') { await hatched.eggs.updateStatus(egg.eggId, 'ready'); } const hatchOp = await hatched.eggs.hatch(egg.eggId); const finished = await hatched.operations.wait(hatchOp.operationId, { timeoutMs: 60_000 }); const buddyId = finished.result.buddyId; console.log('Buddy ready:', buddyId); // Persist buddyId against your app user — you need it for the widget session below // and on every future page load. (See "Persist buddy_id" in the bootstrap guide.) ``` ```bash # 1. Create-or-reuse an egg for this user (ensure is a query param) EGG=$(curl -sX POST "https://api.hatched.live/api/v1/eggs?ensure=true" \ -H "Authorization: Bearer $HATCHED_API_KEY" \ -H "Content-Type: application/json" \ -d '{"user_id":"user_42"}') EGG_ID=$(echo "$EGG" | jq -r .egg_id) # 2. Move egg to ready (only when status == waiting) curl -sX PATCH "https://api.hatched.live/api/v1/eggs/$EGG_ID/status" \ -H "Authorization: Bearer $HATCHED_API_KEY" \ -H "Content-Type: application/json" \ -d '{"status":"ready"}' # 3. Hatch — returns an async operation OP=$(curl -sX POST "https://api.hatched.live/api/v1/eggs/$EGG_ID/hatch" \ -H "Authorization: Bearer $HATCHED_API_KEY") OP_ID=$(echo "$OP" | jq -r .operation_id) # 4. Poll until done (typically 5–45s) while :; do STATUS=$(curl -s -H "Authorization: Bearer $HATCHED_API_KEY" \ "https://api.hatched.live/api/v1/operations/$OP_ID" | jq -r .status) [ "$STATUS" = "completed" ] && break if [ "$STATUS" = "failed" ]; then echo "hatch failed" >&2; exit 1; fi sleep 2 done # 5. Read the buddy_id from the finished operation curl -s -H "Authorization: Bearer $HATCHED_API_KEY" \ "https://api.hatched.live/api/v1/operations/$OP_ID" | jq -r .result.buddy_id ``` ```py import os, time, requests base = "https://api.hatched.live/api/v1" headers = { "Authorization": f"Bearer {os.environ['HATCHED_API_KEY']}", "Content-Type": "application/json", } egg = requests.post(f"{base}/eggs?ensure=true", json={"user_id": "user_42"}, headers=headers).json() if egg["status"] == "waiting": requests.patch(f"{base}/eggs/{egg['egg_id']}/status", json={"status": "ready"}, headers=headers).raise_for_status() op = requests.post(f"{base}/eggs/{egg['egg_id']}/hatch", headers=headers).json() while True: result = requests.get(f"{base}/operations/{op['operation_id']}", headers=headers).json() if result["status"] == "completed": buddy_id = result["result"]["buddy_id"] break if result["status"] == "failed": raise RuntimeError(result.get("error")) time.sleep(2) print(f"Buddy ready: {buddy_id}") ``` Image generation runs asynchronously; `operations.wait` polls the hatch operation until the buddy's art is ready (typically 5–45 seconds). Show a loading state in your UI rather than blocking on it. ## 4. Confirm the event type and send your first event The preset/plan in step 1 should already have registered `lesson_completed`. If you changed the event name, confirm the same type exists in the dashboard before sending it. An unregistered type fails with `event_type_not_registered` instead of silently doing nothing. ```ts const effects = await hatched.events.send({ eventId: 'lesson_lsn_1_user_42', userId: 'user_42', type: 'lesson_completed', properties: { lessonId: 'lesson_1', durationMs: 5 * 60 * 1000 }, }); console.log(effects); if (effects.debugReason) { console.log('Accepted, but no visible effect yet:', effects.debugReason); } ``` ```bash FIRST_EVENT=$(curl -sS -X POST https://api.hatched.live/api/v1/events \ -H "Authorization: Bearer $HATCHED_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "event_id": "lesson_lsn_1_user_42", "user_id": "user_42", "type": "lesson_completed", "properties": { "lesson_id": "lesson_1", "duration_ms": 300000 } }') echo "$FIRST_EVENT" | jq . echo "$FIRST_EVENT" | jq -e '.accepted == true' ``` ```py response = requests.post( "https://api.hatched.live/api/v1/events", headers=headers, json={ "event_id": "lesson_lsn_1_user_42", "user_id": "user_42", "type": "lesson_completed", "properties": {"lesson_id": "lesson_1", "duration_ms": 300_000}, }, timeout=10, ) response.raise_for_status() effects = response.json() print(effects) ``` Success is `accepted: true` plus an `effects` object. If the event is accepted but no visible state changes, use the debug reason instead of guessing: - `no_active_buddies_for_user` means the `user_id`/audience has no active buddy yet. Reuse the `buddyId` from step 3, or hatch one before testing events. - `no_matching_rules` means the event type exists, but your published rules do not award coins, badges, streak progress, path progress, or evolution for it. - A `400 event_type_not_registered` response means the plan/preset was not applied for that audience, or the event name does not match the registered type. Then open **Settings → Event Log** and confirm the same `event_id` appears with the `effects`/`debug_reason` payload. Analytics updates from the same accepted event, so you should see it in the dashboard after ingestion. Full event ingestion guide (batch mode, idempotency, ordering) → [Send events](/docs/guides/send-events). The [rule engine](/docs/concepts/rule-engine) evaluates the event against the buddy's pinned config and applies coin, skill, badge, streak, and evolution effects in a single transaction. `eventId` provides idempotency — re-sending the same id returns the cached effect without re-applying rules. When an event satisfies the next evolution condition, the SDK response includes `effects.evolutionReady === true`. If your config does not enable auto-evolve, start the stage transition from your backend: ```ts const effects = await hatched.events.send({ eventId: 'lesson_lsn_2_user_42', userId: 'user_42', type: 'lesson_completed', }); if (effects.evolutionReady) { const evolveOp = await hatched.buddies.evolve('bdy_abc'); await hatched.operations.wait(evolveOp.operationId); } ``` ## 5. Embed the buddy widget On any page your user visits, mint a **widget session token** on your server, using the `buddyId` you stored in step 3: ```ts const session = await hatched.widgetSessions.create({ buddyId, // from the hatch result / your stored value — NOT the userId userId: 'user_42', scopes: ['read', 'events:track', 'marketplace:browse'], ttlSeconds: 60 * 15, }); ``` This is the *interactive* token (`data-session-token`). For a purely read-only display mount, use `embedTokens.create(...)` instead (`data-embed-token`, no scopes) — see [Auth model](/docs/concepts/auth-model#session-token-vs-embed-token). Pass the token to the client and render the widget: ```html
``` That's it. The widget mounts in a Shadow DOM, pulls buddy state, and reflects new events in real time. ## Next steps - [Handle webhooks](/docs/guides/handle-webhooks) — react on your backend when a buddy earns a badge or hits a streak milestone. - [Configure rules](/docs/guides/configure-rules) — tune the coin economy and badge conditions. - [Reference](/docs/reference/http-api) — the full API spec. - [Auth model](/docs/concepts/auth-model) — secret vs publishable keys. --- # First user bootstrap > The complete first-run path — from a published config to a mounted widget — in both the SDK and raw HTTP. The one flow you can't skip steps in. Source: https://docs.hatched.live/docs/guides/first-user-bootstrap A widget token is scoped to a **buddy**, and a buddy starts life as an **egg**. So before you can render anything for `user_42`, you have to walk a short chain: ```text published config → reuse-or-create egg → mark ready → hatch → poll the hatch operation → read & persist buddy_id → widget session token → mount widget.js ``` You can't shortcut from `user_id` straight to a widget session token — `POST /widget-sessions` requires an existing `buddy_id`. This page is that chain, end to end, idempotency and latency included. (For everyday calls once the buddy exists, see [Getting started](/docs/guides/getting-started); for the full SDK surface, [SDK quickstart](/docs/guides/sdk-quickstart).) > **Casing:** the raw HTTP API is `snake_case` (`user_id`, `buddy_id`, > `ttl_seconds`). The SDK is the only place you write camelCase — it converts on > the wire. The ` ```http ` blocks below are `snake_case`; the ` ```ts ` blocks > are SDK calls. ## 0. Publish a config version first Picking a dashboard preset during onboarding publishes your first config version automatically — so most integrations never see this. You hit it only if you built a config from scratch or haven't published yet: `POST /eggs` then returns `409` with `error.code: "no_published_config"` (SDK: `NoPublishedConfigError`), and `error.details.publish_url` links straight to the dashboard publish page. Open the rules editor and hit **Publish** — that freezes your draft into the snapshot every new buddy gets pinned to. Three follow-on gotchas: - **Draft vs published.** Streaks, paths, badges, coin rules, and marketplace items only reach widgets once they're in the *published* snapshot. If the dashboard shows an active `daily_quizzer` streak but `/widget/streak/daily_quizzer` returns `404`, that definition is still on a *draft* config — publish again. - **Existing buddies stay pinned.** Publishing a new version does not move buddies you already created; they keep running on their old (possibly empty) snapshot until you migrate them from the dashboard. During integration testing it's usually cleanest to publish first, then create a fresh test buddy. - **Key / environment match.** `hatch_test_*` keys talk to staging, `hatch_live_*` to production — and a config published in one environment isn't visible from the other. Make sure your key and your published config are in the same place. ## 0.5 Want a test player without the chain? Use Player Zero If you just need *something to send events at* while wiring up your backend, skip the egg chain entirely: every workspace has a reserved demo player — **Player Zero**, `user_id "player-0"` — provisioned in one idempotent call. It's the same buddy every dashboard widget preview binds to, so events you send as `player-0` show up there immediately and never pollute real user data. ```ts const { buddy } = await hatched.players.zero(); // create-or-get, instant await hatched.events.ingest({ userId: buddy.userId, // "player-0" type: 'lesson.completed', eventId: 'evt_demo_1', }); ``` ```http POST /api/v1/players/zero Authorization: Bearer hatch_test_… # → 201 { "created": true, "buddy": { "id": "…", "user_id": "player-0", … } } # (second call → "created": false, same buddy) ``` New Player Zero buddies return immediately on a safe placeholder image while Hatched queues their first brand-styled base render in the background. If the first response still carries the placeholder `image_url`, widgets can render it safely and will pick up the brand-styled image on their normal refresh path after generation completes. `GET /api/v1/players/zero` reads its status (`exists` / `hatched`) without creating it. The rest of this page is the chain for **real** users. ## 1. Reuse an existing buddy before creating anything The cardinal rule: **one buddy per (customer, user) — look it up before you create.** React Strict Mode, focus re-fetches, hot reloads, and retry logic all love to call your bootstrap twice; without a guard you'll create a second egg, then a third, and eventually hit `409` with `error.code: "active_egg_limit"` (SDK: `ActiveEggLimitError`) — `error.details.active` then lists the eggs you already have, and `error.details.max` the cap. (`POST /eggs?ensure=true` reuses one of those instead of failing — see §2.) **With the SDK:** ```ts import { HatchedClient } from '@hatched/sdk-js'; const hatched = new HatchedClient({ apiKey: process.env.HATCHED_API_KEY! }); async function ensureBuddyId(userId: string): Promise { // 1. Already stored against this app user? Use it. const stored = await loadStoredBuddyId(userId); // your DB column / profile field / localStorage if (stored) return stored; // 2. Hatched already has a buddy for this user? Reuse it. const existing = await hatched.buddies.list({ userId, status: 'active' }); if (existing.data.length > 0) { const buddyId = existing.data[0].id; await saveStoredBuddyId(userId, buddyId); return buddyId; } // 3. Nothing yet — create one (next section). return createAndHatch(userId); } ``` **With raw HTTP:** ```http GET /api/v1/buddies?user_id=user_42&status=active Authorization: Bearer hatch_test_… # → { "data": [ { "id": "…", "user_id": "user_42", … } ], "meta": { … } } # If data is non-empty, store data[0].id and skip egg creation. ``` ## 2. Create the egg, mark it ready, hatch Only reach this when §1 found nothing. **Guard the create call so it runs at most once per user** — a module-level in-flight map, a DB unique constraint on `(app_user_id)`, or a key you generate yourself (`egg:bootstrap:user_42`). Don't put it anywhere a React effect, focus handler, or hot reload will re-run it. Pass **`ensure: true`** (raw HTTP: `?ensure=true`): instead of always creating a new egg, it returns this user's most recent `waiting`/`ready` egg if one already exists. That makes the call idempotent for the bootstrap path and means a retry after a crashed first attempt picks the half-finished egg back up instead of hitting the active-egg cap. **With the SDK:** ```ts async function createAndHatch(userId: string): Promise { // ensure:true → reuse this user's existing waiting/ready egg if there is one. const egg = await hatched.eggs.create({ userId, ensure: true }); // POST /eggs?ensure=true if (egg.status === 'waiting') { await hatched.eggs.updateStatus(egg.eggId, 'ready'); // PATCH /eggs/:id/status } const op = await hatched.eggs.hatch(egg.eggId); // POST /eggs/:id/hatch → { operationId } // Hatch is async — image generation runs 5–45s. Don't block the UI on it. const result = await hatched.operations.wait(op.operationId, { intervalMs: 2_000 }); if (result.status !== 'completed') throw new Error(`hatch ${result.status}`); const buddyId = result.result.buddyId; await saveStoredBuddyId(userId, buddyId); // ← persist immediately return buddyId; } ``` **With raw HTTP:** ```http POST /api/v1/eggs?ensure=true { "user_id": "user_42", "metadata": {} } # → { "egg_id": "…", "status": "waiting", "buddy_id": null, … } # (status may already be "ready" if you're reusing an egg — skip the PATCH then.) PATCH /api/v1/eggs/{egg_id}/status { "status": "ready" } POST /api/v1/eggs/{egg_id}/hatch # → { "operation_id": "op_…", "status": "pending" } # Poll every ~2s until status is "completed" (typically 5–45s): GET /api/v1/operations/{operation_id} # → { "operation_id": "op_…", "status": "completed", "result": { "buddy_id": "…" }, … } ``` `GET /api/v1/eggs/{egg_id}` (and `GET /api/v1/eggs`) echo `buddy_id` once the egg reaches `status: "hatched"` — it's `null` before that. Persisting the `result.buddy_id` from the operation here is still the right move (you have it sooner, and you avoid an extra round trip). While the hatch operation is `pending`, show a loading state — a placeholder egg, a spinner, "your buddy is hatching…". Don't block the first widget render indefinitely; mount it once you have `buddy_id` and a session token, and let the widget show its own loading state for the artwork. ## 3. Mint the widget session token `POST /widget-sessions` needs the `buddy_id` from step 2 (or §1) **and** `scopes`. Send all the scopes the widgets on that page actually use. **With the SDK:** ```ts const session = await hatched.widgetSessions.create({ buddyId, userId: 'user_42', scopes: ['read', 'events:track', 'marketplace:browse', 'marketplace:purchase', 'items:equip'], ttlSeconds: 3600, }); // → { token, sessionId, expiresAt, scopes } ``` **With raw HTTP:** ```http POST /api/v1/widget-sessions { "user_id": "user_42", "buddy_id": "uuid-from-operation-result", "scopes": ["read", "events:track", "marketplace:browse", "marketplace:purchase", "items:equip"], "ttl_seconds": 3600 } # → { "token": "wgt_…", "session_id": "…", "expires_at": "…", "scopes": [ … ] } ``` This is **not** the embed-token endpoint. `POST /embed-tokens` is the *read-only* path — it rejects `scopes` and returns `mode: read-only`. Use a session token (`data-session-token`) when the widgets need to track events, purchase, or equip; use an embed token (`data-embed-token`) only for purely display mounts. See [Auth model](/docs/concepts/auth-model#session-token-vs-embed-token). ## 4. Mount the widget ```html
``` The token is short-lived; re-mint it on each page load (or just before it expires). The widget reads buddy state from `/widget/state` and renders artwork when it's ready. ## Persist `buddy_id` — this is not optional After the first successful hatch, **store `buddy_id` against your app user** and read it back on every subsequent load: - **Authenticated apps** — a column or profile-metadata field on your user record (`users.hatched_buddy_id`). - **Anonymous demos** — `localStorage` keyed by your local user id. On every widget load: stored `buddy_id` → use it; else `GET /buddies?user_id=…` → reuse the first active buddy; **only then** create an egg. Never create an egg on app mount, focus, hot reload, or a failed widget mount. ## Common pitfalls | Symptom | Cause | Fix | | --- | --- | --- | | `property userId should not exist` | Sent camelCase to the raw HTTP API | The raw API is `snake_case` (`user_id`). Use `snake_case`, or use `@hatched/sdk-js` (it converts for you). | | `buddy_id must be a UUID` | Passed `user_id` (or nothing) where `buddy_id` was expected | Mint the session with the `buddy_id` from the hatch operation's `result`, not the `user_id`. | | `property scopes should not exist` | Sent `scopes` to `POST /embed-tokens` | That's the read-only endpoint. Use `POST /widget-sessions` for scoped/interactive tokens. | | `409 no_published_config` (`NoPublishedConfigError`) | No published config yet | Publish a config version in the dashboard — `err.details.publish_url` links there. Check your key's environment matches where you published. | | `409 active_egg_limit` (`ActiveEggLimitError`) | Bootstrap ran multiple times (Strict Mode, focus, retries) and filled the per-user egg cap | `err.details.active` lists the existing eggs — hatch or cancel one, or retry the create with `?ensure=true` to reuse it. Better: guard egg creation and reuse via stored `buddy_id` → `GET /buddies?user_id=…` before ever calling `POST /eggs`. | | `/widget/streak/` or `/widget/path/` returns 404 although the dashboard shows the definition | The definition is on a *draft* config, not the published snapshot | Publish again so the snapshot includes it; migrate or recreate the buddy if it was pinned before the publish. | | Hatch takes 20–45s and the UI hangs | Treating hatch as synchronous | It's an operation — poll `GET /operations/:id` every ~2s, show a loading state, mount the widget once `buddy_id` is available. | ## Related - [Getting started](/docs/guides/getting-started) — the happy path once the buddy exists. - [Auth model](/docs/concepts/auth-model) — session token vs embed token vs publishable key. - [Best practices](/docs/guides/best-practices) — idempotency, multi-tenant ids, and more. - [Troubleshooting](/docs/guides/troubleshooting) — diagnosing the errors above. --- # SDK quickstart > Install @hatched/sdk-js, authenticate, and make your first calls from Node or TypeScript. Source: https://docs.hatched.live/docs/guides/sdk-quickstart `@hatched/sdk-js` is the official TypeScript SDK for the Hatched API. It ships as dual ESM + CJS, runs on Node 18+, Cloudflare Workers, Vercel Edge, Deno, and Bun. Full package on [npmjs.com/package/@hatched/sdk-js](https://npmjs.com/package/@hatched/sdk-js). ## Install ```bash pnpm add @hatched/sdk-js # or npm install @hatched/sdk-js # or yarn add @hatched/sdk-js # or bun add @hatched/sdk-js ``` ## Set the secret key as an environment variable Add the key to your environment — never hard-code it. ```bash # .env (Express), Vercel/Render dashboard, etc. HATCHED_API_KEY=hatch_test_... ``` A literal key in source control is the most common Hatched onboarding incident — it's the one mistake that's hard to undo (rotate the key, audit access). The SDK reads the variable at construction; never log the key value. For **Next.js App Router**, the key belongs in `.env.local` (not committed) and is referenced from server components and route handlers only. Client components that need to call Hatched should call your route handler, which holds the secret. ## Initialise a client ```ts import { HatchedClient } from '@hatched/sdk-js'; const hatched = new HatchedClient({ apiKey: process.env.HATCHED_API_KEY!, // optional overrides: baseUrl: 'https://api.staging.hatched.live/api/v1', timeoutMs: 15_000, maxRetries: 3, fetch: globalThis.fetch, }); ``` `hatch_test_*` secret keys default to the staging API if you omit `baseUrl`; `hatch_live_*` keys default to production. The SDK parses the [canonical error envelope](/docs/reference/error-codes) and throws typed `HatchedError` subclasses with `requestId`, `code`, and `statusCode` fields. > Secret keys (`hatch_live_*`, `hatch_test_*`) are **server-only**. The > SDK throws if instantiated in a DOM environment. See > [Auth model](/docs/concepts/auth-model) for browser options. ## Core resources ```ts // Eggs & buddies // Pass ensure: true on a first-run bootstrap so a stale egg is reused instead // of throwing active_egg_limit — see /docs/guides/first-user-bootstrap. const egg = await hatched.eggs.create({ userId, ensure: true }); await hatched.eggs.updateStatus(egg.eggId, 'ready'); await hatched.eggs.hatch(egg.eggId); await hatched.buddies.list({ userId }); // Economy await hatched.buddies.earn(buddyId, { amount: 50, reason: 'lesson_reward' }); await hatched.buddies.spend(buddyId, { amount: 20, reason: 'item_purchase' }); const equip = await hatched.buddies.equip(buddyId, { equip: [itemId] }); if (equip.operationId) await hatched.operations.wait(equip.operationId); const buddy = await hatched.buddies.get(buddyId); console.log(buddy.appearance?.status); // Recovery only: use when buddy.appearance?.error?.code === 'needs_rerender' // await hatched.buddies.rerenderAppearance(buddyId); // Events await hatched.events.send({ eventId, userId, type, properties }); // Operations (async image jobs) const op = await hatched.eggs.hatch(egg.eggId); const finished = await hatched.operations.wait(op.operationId); // Widget sessions await hatched.widgetSessions.create({ buddyId, userId, scopes, ttlSeconds }); await hatched.widgetSessions.revoke(sessionId); ``` ## Error handling ```ts import { HatchedError, RateLimitError, ValidationError } from '@hatched/sdk-js'; try { await hatched.events.send({ ... }); } catch (err) { if (err instanceof RateLimitError) { await sleep(err.retryAfter * 1000); } else if (err instanceof ValidationError) { console.error('bad payload:', err.details); } else if (err instanceof HatchedError) { console.error(err.code, err.requestId, err.message); } else { throw err; } } ``` The SDK exposes a typed class per known error code. See [Error codes](/docs/reference/error-codes) for the full catalogue. ## Retries and idempotency - `events.send` is idempotent on `eventId` — passing the same id twice returns the cached effect without re-applying rules. - Accepted events that do not change visible state include `effects.debugReason` (`no_active_buddies_for_user` or `no_matching_rules`) so your integration can show the right next step instead of treating the request as failed. - The client retries `GET`s and `idempotent: true` `POST`s on network failures, `408`, `429` (with `Retry-After` honoured), and 5xx responses. Exponential backoff + jitter. - 4xx responses (other than 408/429) surface immediately. ## Cancellation Every resource method accepts an optional `AbortSignal` as its last argument. The SDK combines it with the built-in timeout via `AbortSignal.any`, so either source can cancel the request. ```ts const controller = new AbortController(); setTimeout(() => controller.abort(), 500); await hatched.buddies.get(buddyId, controller.signal); ``` ## Rate limits + request ids ```ts await hatched.events.send({ ... }); console.log(hatched.getRateLimitInfo()); // { limit: 1000, remaining: 986, reset: 1735689600, retryAfter: undefined } console.log(hatched.getLastRequestId()); // 'req_abc_123' ``` Include the request id in any support ticket and we can look up the full trace. ## Next steps - [Concepts overview](/docs/concepts/overview) — the motivational layer primitives (Mission Anchor, Hatch Ceremony, LEAGUES, the Octalysis Planner) the SDK is shaped around. - [Widget integration guide](/docs/guides/widget-integration) — every widget surface, every prop, the full theming surface. - [Auth model — secret vs publishable keys](/docs/concepts/auth-model) — the decision tree for when you reach for a publishable key instead of a secret key. - [Webhooks](/docs/guides/handle-webhooks) — signature verification, retry semantics, and the event catalogue. - [SDK reference](/docs/reference/sdk-js) — every resource, every method, every option. - [Error codes](/docs/reference/error-codes) — the typed error catalogue and what to do about each one. - [Use Hatched with AI coding assistants](/docs/ai-assistants) — drop the `AGENTS.md` into your repo, point your assistant at `/llms-full.txt`, and let it write the integration for you. --- # Widget integration > Drop Hatched widgets into any page with one loader script and stable data-hatched-mount attributes. Source: https://docs.hatched.live/docs/guides/widget-integration Widgets are the presentation layer: small Shadow DOM UI pieces that render buddy state directly in your product without leaking CSS in either direction. ## The short version ```html
``` The loader reads the token from its own script tag, discovers known `data-hatched-mount` elements, downloads only the bundles present on the page, and keeps them in sync through one shared widget state poller. The browser global is `window.__HATCHED_WIDGET__`. If a SPA creates mount elements after the loader has already run, call `window.__HATCHED_WIDGET__?.init({ token })` after those elements exist. The required identity field is `token`; user and buddy identity come from the JWT. Advanced endpoint and theme overrides are documented in [Runtime configuration](#runtime-configuration). ## From onboarding to production Onboarding scans the operator's site, extracts a brand brief plus visual identity evidence (palette, typography, motifs), and seeds three widget defaults on the customer: `widget_theme_config`, `widget_custom_css`, and `widget_size`. The Dashboard Widget Studio preview and the install snippets both read those same settings, so the widget a teammate approves in the dashboard is the widget that ships in production. > **The shortest path:** open Widget Studio → pick a preset (or click **AI from > theme** so the loader rebuilds the theme from your scanned brand evidence) → > dial in personality, size, and CSS hooks → **Save**. Your live page picks up > the new theme on next load or focus — see [Live theme sync](#live-theme-sync) > below. Use the dashboard-generated snippet as the source of truth when possible. It includes the current personality axes, theme variables, custom CSS hook overrides, size, and the correct mount id for each widget. Manual edits should use the public `data-*` attributes, `--hw-*` variables, and `.hw-*` class hooks below so future widget releases can keep the same customization contract. ## Live theme sync The loader fetches `/widget/theme` on init (using the same widget token you already pass) and on browser focus / tab visibility change. The response carries the customer's current `widget_theme_config` (preset, personality, vars), `widget_custom_css`, and `widget_size`. If anything differs from the inline `data-*` attributes, the loader hot-swaps the widget — surface, geometry, motion, reward voice, iconography, typography, palette and density all update without a page reload. What this means in practice: - **You do not have to re-paste the snippet** every time a teammate changes a color or switches a personality dial in Widget Studio. - The inline `data-surface` / `data-geometry` / `data-motion-profile` / `data-reward-voice` / `data-iconography` / `data-typography` / `data-theme-vars` / `data-custom-css` attributes still ship as **fast-paint fallbacks** so SSR/no-JS or first-render scenarios get the right widget immediately, but the API answer is authoritative. - Theme refresh respects a 5s minimum interval and a 15s `Cache-Control` on the endpoint so it stays cheap even on heavily-trafficked partner pages. ### Theme inheritance precedence When the same theme attribute is set in more than one place, Hatched resolves in this order — last write wins: 1. **Onboarding scan defaults** — the brand brief seeded at signup (palette, typography, motifs). Lowest priority; only used when nothing else is set. 2. **Widget Studio saved theme** — what a teammate approved in the dashboard. Persisted on the customer and served by `/widget/theme`. This is the *production* answer. 3. **Inline `data-*` attributes** on the loader `
``` See [Auth model → Embed token](/docs/concepts/auth-model#embed-token) for the full lifecycle, raw-HTTP form, and how it differs from a widget session token. ### Token matrix The widget session token is the default install — it covers everything below. The embed column only marks the displays that *also* work read-only. | Widget / action | Token to use | | ------------------------------ | --------------------------------------------- | | Hatch ceremony (egg → buddy) | Widget session token **only** — never plays on an embed token | | Buddy display | Widget session token; embed token if the buddy is already hatched | | Badges display | Widget session token or embed token | | Leaderboard display | Widget session token or embed token | | Streak display | Widget session token or embed token | | Guided path display | Widget session token or embed token | | Browser `track()` | Widget session token with `events:track` | | Manual path sub-step completion | Widget session token with `events:track` | | Marketplace browse | Widget session token with `marketplace:browse` (embed token shows a read-only catalog) | | Marketplace purchase | Widget session token with `marketplace:purchase` | | Equip / unequip items | Widget session token with `items:equip` | | Kudos send / quest join / box open | Widget session token with the matching scope — returns `403` on an embed token | ### Allowed browser origins Widget runtime requests are allowed per customer. When onboarding is seeded from a website URL, Hatched automatically adds that URL's origin to `settings.widget_allowed_origins`. You can add local, staging, and production app origins later in Dashboard → Settings → General → Widget allowed origins. The allowlist is read from customer settings on each widget API request; it is not baked into embed or session tokens. Use origins only, not full paths: ```txt https://app.example.com http://localhost:4002 ``` ### Staging vs production The loader picks its default API base from the URL it was loaded from: | Loader URL | Default API base | | ------------------------------------------- | --------------------------------------------- | | `https://cdn.hatched.live/widget.js` | `https://api.hatched.live/api/v1` | | `https://cdn.hatched.live/staging/widget.js` | `https://api.staging.hatched.live/api/v1` | So a snippet that points at the staging CDN automatically talks to the staging API — no `data-api-base-url` override needed. If you _do_ pass `data-api-base-url`, the value must be one of the canonical Hatched API origins (`api.hatched.live`, `api.staging.hatched.live`); arbitrary values are rejected to prevent token exfiltration via host-page XSS. ## Runtime configuration The script tag is the preferred public configuration surface. The same values can be passed to `window.__HATCHED_WIDGET__?.init(...)` when a SPA mints or refreshes a token after hydration. ```ts window.__HATCHED_WIDGET__?.init({ token, apiBaseUrl: 'https://api.staging.hatched.live/api/v1', cdnBaseUrl: 'https://cdn.hatched.live/staging/widget/v1/', themeVars: { '--hw-accent': '#3F8F5F' }, customCss: '.hw-container { box-shadow: none; }', personality: { surface: 'paper', geometry: 'rounded', motion_profile: 'standard', reward_voice: 'joyful', iconography: 'geometric', typography_pair: 'sans-serif-modern', }, size: 'medium', lang: 'en', }); ``` | Init key | Equivalent script attribute | Notes | | -------------- | --------------------------- | ----- | | `token` | `data-session-token` or `data-embed-token` | Required unless already present on the script tag | | `apiBaseUrl` | `data-api-base-url` | Must be a canonical Hatched API origin | | `cdnBaseUrl` | Derived from script `src` | Advanced staging / self-hosted bundle override | | `themeVars` | `data-theme-vars` | JSON object of `--hw-*` values | | `customCss` | `data-custom-css` / `data-custom-css-id` | Inline CSS or CSS script element | | `personality` | `data-surface`, `data-geometry`, etc. | Widget Studio writes these for you | | `size` | `data-size` | `small`, `medium`, or `large` | | `lang` | `data-lang` | Locale hint | Per-widget instance identity belongs on the mount element. For example, streak widgets read `data-streak-key` and path widgets read `data-path-key` from the `
`, not from `init()`. ## Available mounts | Mount attribute | Purpose | | --------------------------------------- | -------------------------------------------------- | | `data-hatched-mount="buddy"` | Animated companion, coins, stage, equipped items | | `data-hatched-mount="badges"` | Earned and locked badge shelf | | `data-hatched-mount="streak"` | One or more streak counters; add a Dashboard streak `key` via `data-streak-key` | | `data-hatched-mount="path"` | Guided journey for the audience's active path; add `data-path-key` to pin a specific path | | `data-hatched-mount="tokens"` | Wallet card — spendable balance plus progression-token balances with progress toward each gate | | `data-hatched-mount="marketplace"` | Browse, buy, and equip items | | `data-hatched-mount="leaderboard"` | Community rank surface | | `data-hatched-mount="kudos"` | Peer recognition send + receive | | `data-hatched-mount="group-quest"` | Team-scoped quest progress (Growth+) | | `data-hatched-mount="feed"` | Passive team event feed | | `data-hatched-mount="mystery-box"` | Skinner-box surprise reward (Growth+) | | `data-hatched-mount="league"` | Season tier + cohort standing (Growth+) | | `data-hatched-mount="council"` | UGC narrative co-authoring (Enterprise) | | `data-hatched-mount="hexad-survey"` | Onboarding player-type survey | Legacy ids such as `id="buddy-widget"` still work for older installs, but new snippets should use `data-hatched-mount`. Gated widgets still mount on every plan; the API responds with `403 plan_feature_locked` until the customer upgrades, and the widget renders a locked-state placeholder pointing at billing. The error details carry `required_plan` and an `upgrade_url` so the placeholder can link straight to the right upgrade. The full plan/capability ground truth lives in [Plan capabilities](/docs/reference/plan-capabilities). ## Bundle sizes and loading The 6 KB loader (`widget.js`) is all your page ships up front. Each widget bundle is fetched on demand the first time its mount is found in the DOM, and the Preact runtime plus shared UI code live in **separate cached chunks** that every widget reuses — so adding a second or third widget to a page costs only its own entry, not another copy of the framework. | Widget | First load (gzip) | Widget | First load (gzip) | | ------------- | ----------------- | ------------- | ----------------- | | `buddy` | ~87 KB | `path` | ~49 KB | | `marketplace` | ~71 KB | `hexad-survey`| ~48 KB | | `leaderboard` | ~61 KB | `group-quest` | ~47 KB | | `badges` | ~56 KB | `feed` | ~46 KB | | `kudos` | ~50 KB | `streak` | ~45 KB | | `tokens` | ~45 KB | `council` | ~43 KB | | `league` | ~44 KB | `mystery-box` | ~41 KB | "First load" is the entry bundle plus the shared chunks it pulls, gzipped — the worst case of a cold cache with that one widget alone on the page. The shared chunk pool is ~65 KB gzipped in total and is downloaded **once per page**, so a page with three widgets transfers roughly _(sum of the three entries) + (the shared pool, once)_ rather than the sum of three "first load" figures. Repeat visits and incremental deploys reuse content-hashed chunks straight from the browser cache; only a chunk whose contents actually changed is re-fetched. You don't host or version these files — they're served from `cdn.hatched.live`, and the loader you embed decides which bundle build to pull (see [Widget versioning](/docs/guides/widget-versioning)). ## Personality dimensions Hatched widgets compose six personality axes that decide how the widget feels — not just how it's coloured. The Widget Studio presets pick one value per axis; the loader forwards each as a `data-*` attribute that drives Shadow-DOM CSS attribute selectors. You can override any single axis without abandoning the preset. | Axis | Attribute | Values | Drives | | ---------------- | --------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------- | | Surface | `data-surface` | `paper` `glass` `metal` `crt` `parchment` `vellum` | Shell background texture (grain, frost, scanlines) | | Geometry | `data-geometry` | `rounded` `cut` `pill` `sharp` `organic` | Shell, card, button, badge clip + bar radii | | Motion profile | `data-motion-profile` | `calm` `standard` `expressive` `theatrical` | Idle bounce, hover lift, animation amplitude | | Reward voice | `data-reward-voice` | `quiet` `crisp` `joyful` `epic` | Coin pulse, level-up burst, badge glow intensity | | Iconography | `data-iconography` | `geometric` `hand-drawn` `pixel` `embossed` `flat-mono` | Icon stroke, wobble, pixelation, depth | | Typography pair | `data-typography` | `sans-serif-modern` `serif-classic` `mono-tech` `rounded-playful` `display-condensed` | Body + display font stacks | Pick the combination that matches your brand. A fintech onboarding probably wants `vellum + sharp + calm + crisp + geometric + sans-serif-modern`. A gaming studio probably wants `crt + sharp + theatrical + epic + pixel + mono-tech`. A wellness app probably wants `parchment + pill + calm + quiet + hand-drawn + serif-classic`. The Widget Studio's **AI from theme** action and the onboarding scout both populate these axes from your scanned brand evidence, so the preset that ships in production matches the one a teammate approved in the dashboard. ## Styling and CSS hooks Widgets render inside Shadow DOM, so your product CSS cannot accidentally break them. Customization is explicit: pass design tokens on the loader script and use stable `.hw-*` hooks for deeper polish. ```html ``` ### Loader styling attributes | Attribute | Values | Purpose | | --------------------- | ----------------------------------------------------------------------------------- | -------------------------------------------- | | `data-surface` | `paper` `glass` `metal` `crt` `parchment` `vellum` | Shell background texture | | `data-geometry` | `rounded` `cut` `pill` `sharp` `organic` | Corner radii across shell, cards, buttons | | `data-motion-profile` | `calm` `standard` `expressive` `theatrical` | Idle bounce and hover amplitude | | `data-reward-voice` | `quiet` `crisp` `joyful` `epic` | Celebration intensity | | `data-iconography` | `geometric` `hand-drawn` `pixel` `embossed` `flat-mono` | Icon styling | | `data-typography` | `sans-serif-modern` `serif-classic` `mono-tech` `rounded-playful` `display-condensed` | Body + display font pair | | `data-size` | `small` `medium` `large` | Widget density and panel height | | `data-theme-vars` | JSON object of `--hw-*` values | CSS variables injected into each shadow root | | `data-custom-css` | CSS string | Inline custom CSS for short overrides | | `data-custom-css-id` | element id | Reads CSS from a `
``` The loader's bundle base is **URL-derived**: a pinned loader pulls every widget bundle from the same versioned tree, so a `v/0.4.2/widget.js` install only ever fetches `v/0.4.2/widget/v1/buddy-widget.min.js`. There is no way for a pinned page to accidentally load a newer widget bundle. ## Verifying the pin took effect The loader exposes its own version at runtime: ```js window.HatchedWidgets.version; // → "0.4.2" window.HatchedWidgets.buildId; // → "abc123def456" ``` Use these in: - **CI smoke tests** — assert the deployed pin resolved to the expected version. - **Error reports** — include both fields in any client-side error pipeline so you can correlate field bugs to a specific deploy. ## Choosing a version The loader version comes from `apps/widgets/package.json`, and each release is mirrored to `/v//` at deploy time. The [changelog](/docs/reference/changelog) lists each release. The latest unversioned `widget.js` is always the head of `main`. A safe pinning policy: - **Pin to the latest version when you cut a new tenant build.** Don't pin once and forget — pinned loaders never receive security patches. - **Bump within ~30 days of a new minor.** Older pins still serve forever (the objects are immutable), but you'll miss bug fixes. - **Subscribe to the [docs changelog](/docs/reference/changelog).** Every loader release lists what changed, so you can review it before bumping your pin. ## Rollback playbook A pinned loader makes rollback trivial — flip the `src` attribute back to a known-good version and redeploy your host page. Because the older path is still served (immutable), the previous behavior comes back as soon as the new HTML lands. If you're on the unversioned path and need to roll back without waiting for a Hatched deploy, the pinned route is your escape hatch: ```diff - + ``` When the issue is resolved upstream, point the `src` back at the unversioned path or the new version. ## Staging and previews The staging mirror at `cdn.hatched.live/staging/widget.js` exists for testing against the staging API. It does **not** receive pinned version mirrors — staging is short-lived and version freezing it would defeat the purpose. Pin only in production. ## What pinning does NOT pin The loader pin freezes the **client bundle**: the loader, the widget bundles, the locale catalogs. It does **not** pin: - **API responses.** The Hatched API ships forward-compatible changes (additive fields, new event types). Existing fields never change shape within a major version. - **CSS variables consumed by `themeVars`.** New `--hw-*` variables can appear in newer loader versions; pinning to an old loader version means you don't get them automatically. - **Webhook event payloads.** Webhook delivery is server-side. See [webhook payloads](/docs/reference/webhook-payloads) for the contract. If you need a fully end-to-end frozen surface, pin the SDK (`@hatched/sdk-js`) and the loader together — both follow semver. --- # Send events > What to send, when to send it, and how Hatched turns events into effects. Source: https://docs.hatched.live/docs/guides/send-events import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; Events are the only way the outside world changes a buddy. Everything the [rule engine](/docs/concepts/rule-engine) does starts with a `POST /events`. ## Shape ```ts await hatched.events.send({ eventId: 'evt_01HXYZ', // for idempotency userId: 'user_42', type: 'lesson_completed', audience: 'student', // required only if you have 2+ audiences properties: { lessonId: 'lesson_17', durationMs: 5 * 60 * 1000, score: 0.92, }, occurredAt: '2026-04-22T10:30:00Z', // optional; defaults to now }); ``` ```bash curl -X POST https://api.hatched.live/api/v1/events \ -H "Authorization: Bearer $HATCHED_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "event_id": "evt_01HXYZ", "user_id": "user_42", "type": "lesson_completed", "audience": "student", "properties": { "lesson_id": "lesson_17", "duration_ms": 300000, "score": 0.92 }, "occurred_at": "2026-04-22T10:30:00Z" }' ``` ```py import os, requests response = requests.post( "https://api.hatched.live/api/v1/events", headers={ "Authorization": f"Bearer {os.environ['HATCHED_API_KEY']}", "Content-Type": "application/json", }, json={ "event_id": "evt_01HXYZ", "user_id": "user_42", "type": "lesson_completed", "audience": "student", "properties": { "lesson_id": "lesson_17", "duration_ms": 300_000, "score": 0.92, }, "occurred_at": "2026-04-22T10:30:00Z", }, timeout=10, ) response.raise_for_status() effects = response.json() ``` The SDK serialises camelCase field names to snake_case on the wire (`userId` → `user_id`, `occurredAt` → `occurred_at`). When you call the HTTP API directly — curl, Python, Go, Rust — send snake_case yourself. ## Audience Every event belongs to an audience (the role a user plays — `student`, `teacher`, `admin`). The field is `audience` in the SDK (camelCase) and `audience` on the wire (already snake_case). Values are lowercase snake_case, max 32 characters. - **Single-audience customer:** `audience` is optional. Omit it and the server applies your one configured audience as the implicit default. - **Two or more audiences:** `audience` is required. Omit it and the request fails with `400 missing_audience`. Send a value that isn't one of your configured audiences and it fails with `400 unknown_audience`. ## Pick stable event types Event types are the string keys rules match against. Choose them once and don't rename them — existing coin rules, badge conditions, and analytics queries reference them. Use snake_case, present-tense verbs: ``` lesson_completed lesson_started daily_login checkout_completed quiz_passed task_assigned ``` Hatched validates event types before reserving quota. If the type is not registered for the resolved audience, the request fails with `event_type_not_registered`. Applying a dashboard preset or generated plan registers the event types referenced by that plan; custom integrations should create the type before the first production event. ## Properties are yours The rule engine doesn't require a fixed property shape — you define it. Whatever you send becomes queryable via custom conditions and visible in the event log. Stay consistent: if `durationMs` exists for `lesson_completed`, always include it. ## Idempotency Pass a stable `eventId` and you're safe to retry: ```ts await hatched.events.send({ eventId: `lesson_${lessonId}_${userId}`, userId, type: 'lesson_completed', properties, }); ``` Hatched stores `eventId`s per customer and returns the cached effect on duplicate submissions without re-applying rules or charging event quota again. Without `eventId`, retries can produce duplicate effects. ## Order doesn't matter (usually) Events for the same buddy serialise on a row lock. You can send them in parallel — the rule engine will process them one at a time. You don't need a queue on your side unless you want ordering guarantees across different users. ## Batch mode For bulk imports, send up to 100 events in a single request: ```ts await hatched.events.sendBatch([ { eventId: 'e1', userId, type: 'lesson_completed', properties: { ... } }, { eventId: 'e2', userId, type: 'quiz_passed', properties: { ... } }, ]); ``` ```bash curl -X POST https://api.hatched.live/api/v1/events/batch \ -H "Authorization: Bearer $HATCHED_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "events": [ { "event_id": "e1", "user_id": "user_42", "type": "lesson_completed", "properties": {} }, { "event_id": "e2", "user_id": "user_42", "type": "quiz_passed", "properties": {} } ] }' ``` ```py requests.post( "https://api.hatched.live/api/v1/events/batch", headers={ "Authorization": f"Bearer {os.environ['HATCHED_API_KEY']}", "Content-Type": "application/json", }, json={ "events": [ {"event_id": "e1", "user_id": "user_42", "type": "lesson_completed", "properties": {}}, {"event_id": "e2", "user_id": "user_42", "type": "quiz_passed", "properties": {}}, ], }, timeout=15, ).raise_for_status() ``` Each event in the batch carries its own `audience` (camelCase `audience` in the SDK, `audience` on the wire), resolved per event with the same rules as a single `send`: optional for single-audience customers, required once you have two or more. The API validates the whole batch before reserving quota or applying effects. If any event type is not registered for the audience it resolves to, the request fails and no event quota is committed. ## Return shape `send` resolves with the effects the rule engine applied: ```ts const effects = await hatched.events.send({ ... }); console.log(effects); // { // coins: 10, // badgesAwarded: ['first_lesson'], // badgesReady: [], // tokens: [], // evolutionReady: false, // streakMilestones: [], // } ``` Use `effects.badgesAwarded` or `effects.evolutionReady` to trigger celebratory UI on your side the same tick as the event fires. When an event is accepted but produces no visible state change, the response includes a debug reason. In the SDK this appears as `effects.debugReason`; in raw HTTP it is also exposed as top-level `debug_reason`. ```ts const effects = await hatched.events.send({ ... }); if (effects.debugReason === 'no_active_buddies_for_user') { // The user_id/audience has no active buddy yet. } if (effects.debugReason === 'no_matching_rules') { // The event type is registered, but the published rules do not act on it. } ``` --- # Customise the buddy > Style the widget, swap the art style, and tune evolution stages to match your brand. Source: https://docs.hatched.live/docs/guides/customize-buddy The buddy widget is opinionated but configurable. Most products change two things: the art style and the brand tokens. ## Art style At preset selection you pick a **creature style** — cute, sci-fi, fantasy, minimal, playful, or custom. The image generator is primed with this style for egg art, hatch art, and every evolution stage. If none of the presets fit, upload a **style reference image** under Settings → Art style. Hatched will sample the palette, silhouette, and texture characteristics and apply them to generated buddies. ## Brand tokens The widget is themed through `--hw-*` CSS custom properties that you set in the dashboard. The four you'll touch most: - `--hw-accent` — the primary call-to-action colour (coin-earn, hatch button) - `--hw-accent-strong` — hover/pressed state - `--hw-bg` — background - `--hw-text` — text and outlines See [Theme tokens](/docs/reference/theme-tokens) for the full `--hw-*` list. Additional design tokens (radii, shadows, typography) follow a small opinionated set — adjust them in the brand kit, not per page. ## Per-page overrides For dense UIs or embedded contexts, pass an accent override and a size on the loader `
``` The loader injects these `--hw-*` variables into each widget's shadow root. ## Evolution stages Each evolution stage gets a different image. You control the **count** (3–6), the **conditions** (XP, skill level, badges, coins), and whether evolution is automatic or user-triggered. For a complete design conversation with the art generator, see [Evolution](/docs/concepts/evolution). ## Equip slots Marketplace items snap into one of a fixed set of equip slots: head, body, accessory, background. Each item declares which slots it uses. When designing items, stick to the slot's silhouette so the art layering stays clean across evolution stages. --- # Configure rules > Tune the coin economy, skill progression, badge conditions, and streak milestones in the dashboard. Source: https://docs.hatched.live/docs/guides/configure-rules Rules live on your **config version**. Changes land on a draft first, then publish as a new immutable version. Existing buddies stay pinned to their current version until you migrate them. ## Where rules live | Surface | What it controls | | --- | --- | | **Skills → Set** | Skill names, icons, max levels, skill rules | | **Skills → Decay** | Time-based skill loss ([concept](/docs/concepts/skill-decay)) | | **Economy → Coin rules** | Event → coin amount mappings, daily caps | | **Economy → Tokens** | Secondary currencies and their earn rules | | **Engagement → Streaks** | Streak definitions and milestone rewards | | **Engagement → Badges** | Badge conditions, auto-vs-manual award | | **Evolution** | Stage conditions, creature style, art mode | | **Marketplace** | Items, pricing, visibility rules | Each surface writes to the same draft. The diff between draft and published is visible under **Publish** before you flip it live. ## Publishing 1. Review the diff — coin rules changed, badges added, streaks modified. 2. Hit **Publish** — a new immutable version is created and becomes the default for new eggs. 3. Existing buddies are *not* migrated. They stay on their pinned version. 4. Migrate buddies in bulk from **Buddies → Migration** when you're ready. ## Migrating existing buddies Migration is a first-class operation: - It swaps the rulebook, never the state. - Coin balances, badge lists, streak counters all carry over. - If a rule that awarded a badge on their current state no longer exists in the new version, the badge stays — historical awards are never revoked. - You can migrate a single buddy, an audience, or all buddies at once. ## Tuning tips - **Watch Economy Health.** The dashboard shows coin inflow vs. outflow per day. When inflow outruns outflow for too long, marketplace items become invisible. - **Cap the top of the earn curve.** Daily caps prevent grinding; multiplier caps prevent streak-compounding breakage. - **Start with auto-awarded badges.** Manual badges need a moderation workflow — get auto working before adding the human loop. - **Publish small, publish often.** Each version is cheap; big-bang publishes are harder to reason about. --- # Handle webhooks > Verify the HMAC signature, respect the replay window, and respond before Hatched retries. Source: https://docs.hatched.live/docs/guides/handle-webhooks import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; Webhooks are how your backend reacts to buddy events. Hatched signs every request so you can trust the payload came from us and hasn't been tampered with. ## Subscribe 1. Dashboard → Settings → Webhooks → **Add endpoint**. 2. Pick the event types you care about ([catalogue](/docs/reference/webhook-payloads)). 3. Copy the signing secret **once** — we don't show it again. Programmatically: ```ts await hatched.webhooks.create({ url: 'https://your-app.com/api/webhooks/hatched', events: ['buddy.evolved', 'badge.awarded', 'streak.milestone'], }); ``` ## Verify with the SDK helper `@hatched/sdk-js` ships a static helper for signature verification: ```ts import { WebhooksResource } from '@hatched/sdk-js'; export async function POST(req: Request) { const rawBody = await req.text(); const signature = req.headers.get('x-hatched-signature') ?? ''; const timestamp = req.headers.get('x-hatched-timestamp') ?? ''; const valid = WebhooksResource.verifySignature( rawBody, signature, process.env.HATCHED_WEBHOOK_SECRET!, { timestamp }, ); if (!valid) return new Response('invalid signature', { status: 400 }); const event = JSON.parse(rawBody); const deliveryId = req.headers.get('x-hatched-delivery'); const eventType = req.headers.get('x-hatched-event'); if (!deliveryId || !eventType) return new Response('missing metadata', { status: 400 }); await handle({ deliveryId, eventType, payload: event }); return new Response(null, { status: 202 }); } ``` The signature arrives in `X-Hatched-Signature: sha256=` and the timestamp in its own `X-Hatched-Timestamp: ` header. Pass the timestamp via `options.timestamp` — without it the helper fails closed. The helper verifies the HMAC over `` `${timestamp}.${rawBody}` ``, rejects timestamps older than `toleranceSeconds` (default 300), and uses `timingSafeEqual` under the hood. > The framework adapters in `@hatched/sdk-js/webhooks` > (`verifyExpressRequest`, `verifyNextAppRequest`, …) extract both the > signature and the timestamp header for you — reach for them first. > Sign over **raw body bytes**. A JSON `parse`→`stringify` round-trip > reorders keys and breaks the signature. Read the body as `Buffer` or > `string` **before** any framework middleware parses it as JSON. ## Manual verification (without the SDK) The signing scheme is identical across languages: HMAC-SHA256 over `` `${unix_timestamp}.${raw_body_bytes}` `` using the webhook secret. Read the signature from `X-Hatched-Signature` (strip the `sha256=` prefix to get the hex) and the timestamp from `X-Hatched-Timestamp`. Reject anything older than 5 minutes; compare digests in constant time. ```ts import crypto from 'node:crypto'; // signatureHeader = req.headers['x-hatched-signature'] → "sha256=" // timestampHeader = req.headers['x-hatched-timestamp'] → "" export function verifyHatchedSignature( signatureHeader: string, timestampHeader: string, rawBody: Buffer, secret: string, ) { const ts = timestampHeader; const sig = signatureHeader.replace(/^sha256=/, ''); if (!ts || !sig) return false; if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; const expected = crypto .createHmac('sha256', secret) .update(`${ts}.${rawBody.toString('utf8')}`) .digest('hex'); if (sig.length !== expected.length) return false; return crypto.timingSafeEqual( Buffer.from(sig, 'hex'), Buffer.from(expected, 'hex'), ); } ``` ```py import hashlib, hmac, time # signature_header = request.headers['X-Hatched-Signature'] → "sha256=" # timestamp_header = request.headers['X-Hatched-Timestamp'] → "" def verify_hatched_signature( signature_header: str, timestamp_header: str, raw_body: bytes, secret: str, ) -> bool: ts = timestamp_header sig = signature_header.removeprefix('sha256=') if not ts or not sig: return False if abs(time.time() - int(ts)) > 300: return False expected = hmac.new( secret.encode('utf-8'), f"{ts}.".encode('utf-8') + raw_body, hashlib.sha256, ).hexdigest() return hmac.compare_digest(sig, expected) ``` ```go package webhooks import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "strconv" "strings" "time" ) // signatureHeader = r.Header.Get("X-Hatched-Signature") → "sha256=" // timestampHeader = r.Header.Get("X-Hatched-Timestamp") → "" func VerifyHatchedSignature(signatureHeader, timestampHeader string, rawBody []byte, secret string) bool { ts := timestampHeader sig := strings.TrimPrefix(signatureHeader, "sha256=") if ts == "" || sig == "" { return false } tsInt, err := strconv.ParseInt(ts, 10, 64) if err != nil { return false } if delta := time.Now().Unix() - tsInt; delta > 300 || delta < -300 { return false } mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(ts + ".")) mac.Write(rawBody) expected := hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(sig), []byte(expected)) } ``` ## Respond quickly - Return a 2xx within 10 seconds, or Hatched retries the delivery. - Up to 4 attempts (initial + 3 retries at **+5s, +30s, +5min**). After the fourth attempt fails the delivery is marked `failed` in the delivery log, but the state in Hatched is already correct. - If your handler is expensive, ack fast and push to a queue. ## Idempotency Every webhook carries a unique delivery id in the `X-Hatched-Delivery` header. Dedupe against it before side-effects: ```ts if (await alreadyHandled(deliveryId)) return ack(); await recordHandled(deliveryId); await doTheWork(payload); ``` ## Replay from the delivery log Dashboard → Developers → Webhook deliveries shows every attempt with payload, headers, and response. Replay failed deliveries once your endpoint is healthy: ```ts await hatched.webhooks.replay(endpointId, deliveryId); ``` ## Framework examples - [Next.js route handler](/docs/guides/nextjs-integration) — App Router, raw body, signature verify. - [Express middleware](/docs/guides/express-integration) — `express.raw` + signature verify before JSON parsing. - [Edge runtimes](/docs/guides/edge-runtimes) — Workers/Vercel Edge notes. --- # Verify webhooks end-to-end > Copy-paste handlers that capture the raw body, verify the HMAC signature, and acknowledge fast — for Express, Fastify, Hono, Next.js App Router, Next.js Pages Router, and Cloudflare Workers. Source: https://docs.hatched.live/docs/guides/verify-webhooks import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; Every Hatched webhook delivery is signed. Verification is non-negotiable — without it, anyone who guesses your endpoint URL can spoof event traffic. The verification rule is simple: compute `HMAC-SHA256(secret, ${unix_ts}.${rawBody})` and compare it against the hex in the `X-Hatched-Signature: sha256=` header. The `unix_ts` comes from the separate `X-Hatched-Timestamp` header. This guide ships ready-to-paste handlers for the frameworks Hatched users hit most often. Every example follows the same shape: 1. **Capture the raw body** — never the framework's parsed JSON. Any `JSON.parse → JSON.stringify` round-trip reorders keys and breaks the signature. 2. **Verify the signature** — with the SDK adapter for your framework, or the manual HMAC code if you're outside the Node ecosystem. 3. **Dedupe on the `X-Hatched-Delivery` header** — Hatched delivers at-least-once. The same event can arrive twice; your handler must be idempotent. The delivery id lives in the request header, not the body. 4. **Acknowledge fast** — return `2xx` in under 10 seconds. Push slow work to a queue. The signing scheme is exhaustively covered in [Handle webhooks](/docs/guides/handle-webhooks); this page is the framework cookbook. ## SDK adapters `@hatched/sdk-js` ships per-framework adapters that pull the raw body and headers out of the framework's request shape. Every adapter returns the same result: `{ valid, event, eventType, deliveryId, reason }`. ```ts interface VerifyResult { valid: boolean; event: Record | null; // raw parsed payload, no envelope eventType: string | null; // X-Hatched-Event deliveryId: string | null; // X-Hatched-Delivery, dedupe key reason?: | 'missing_header' | 'missing_secret' | 'missing_body' | 'invalid_signature' | 'invalid_json'; } ``` Import from the package root or the `/webhooks` deep import — both expose the same functions. ## Express Capture the body with `express.raw()` *before* any JSON middleware sees the route. Order matters — put the raw-body parser on the webhook path only; keep `express.json()` for the rest of the app. ```ts import express from 'express'; import { verifyExpressRequest } from '@hatched/sdk-js'; const app = express(); const secret = process.env.HATCHED_WEBHOOK_SECRET!; app.post( '/webhooks/hatched', express.raw({ type: 'application/json' }), async (req, res) => { const { valid, event, deliveryId, reason } = verifyExpressRequest(req, secret); if (!valid || !event) { return res.status(400).json({ error: reason ?? 'invalid_signature' }); } if (!deliveryId) return res.status(400).json({ error: 'missing_delivery_id' }); if (await alreadyHandled(deliveryId)) return res.sendStatus(202); await enqueue(event); res.sendStatus(202); }, ); ``` ## Fastify Register a content-type parser that hands you the raw bytes. Without this, Fastify hands you parsed JSON and the signature check fails. ```ts import Fastify from 'fastify'; import { verifyFastifyRequest } from '@hatched/sdk-js'; const fastify = Fastify(); const secret = process.env.HATCHED_WEBHOOK_SECRET!; fastify.addContentTypeParser( 'application/json', { parseAs: 'buffer' }, (_req, body, done) => done(null, body), ); fastify.post('/webhooks/hatched', async (req, reply) => { const { valid, event, deliveryId, reason } = verifyFastifyRequest(req, secret); if (!valid || !event) { return reply.code(400).send({ error: reason ?? 'invalid_signature' }); } if (!deliveryId) return reply.code(400).send({ error: 'missing_delivery_id' }); if (await alreadyHandled(deliveryId)) return reply.code(202).send(); await enqueue(event); reply.code(202).send(); }); ``` ## Hono Hono's `c.req.text()` returns the raw body string — no middleware configuration needed. The adapter is `async` because it awaits the body. ```ts import { Hono } from 'hono'; import { verifyHonoRequest } from '@hatched/sdk-js'; const app = new Hono(); const secret = Deno.env.get('HATCHED_WEBHOOK_SECRET')!; app.post('/webhooks/hatched', async (c) => { const { valid, event, deliveryId, reason } = await verifyHonoRequest(c, secret); if (!valid || !event) { return c.json({ error: reason ?? 'invalid_signature' }, 400); } if (!deliveryId) return c.json({ error: 'missing_delivery_id' }, 400); if (await alreadyHandled(deliveryId)) return c.body(null, 202); await enqueue(event); return c.body(null, 202); }); ``` This is the same shape that runs on Bun, Deno Deploy, Cloudflare Workers, and Vercel Edge — the Web Standards `Request` API is identical across those runtimes. ## Next.js (App Router) In App Router route handlers you receive a standard `Request` — call `req.text()` to get raw bytes. The adapter does this for you. ```ts // app/api/webhooks/hatched/route.ts import { verifyNextAppRequest } from '@hatched/sdk-js'; const secret = process.env.HATCHED_WEBHOOK_SECRET!; export async function POST(req: Request) { const { valid, event, deliveryId, reason } = await verifyNextAppRequest(req, secret); if (!valid || !event) { return Response.json({ error: reason ?? 'invalid_signature' }, { status: 400 }); } if (!deliveryId) { return Response.json({ error: 'missing_delivery_id' }, { status: 400 }); } if (await alreadyHandled(deliveryId)) { return new Response(null, { status: 202 }); } await enqueue(event); return new Response(null, { status: 202 }); } ``` ## Next.js (Pages Router) Pages Router parses the body for you by default. Disable the parser, capture raw bytes manually (e.g. with `raw-body`), then pass them to the adapter. ```ts // pages/api/webhooks/hatched.ts import getRawBody from 'raw-body'; import type { NextApiRequest, NextApiResponse } from 'next'; import { verifyNextPagesRequest } from '@hatched/sdk-js'; export const config = { api: { bodyParser: false } }; const secret = process.env.HATCHED_WEBHOOK_SECRET!; export default async function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method !== 'POST') return res.status(405).end(); const rawBody = await getRawBody(req); const { valid, event, deliveryId, reason } = verifyNextPagesRequest(req, rawBody, secret); if (!valid || !event) { return res.status(400).json({ error: reason ?? 'invalid_signature' }); } if (!deliveryId) return res.status(400).json({ error: 'missing_delivery_id' }); if (await alreadyHandled(deliveryId)) return res.status(202).end(); await enqueue(event); res.status(202).end(); } ``` ## Cloudflare Workers / Edge runtimes Same shape as Hono / App Router — the Web Standards `Request` API works unchanged. Use `verifyNextAppRequest` (it's runtime-agnostic, despite the name). ```ts import { verifyNextAppRequest } from '@hatched/sdk-js'; export default { async fetch(req: Request, env: { HATCHED_WEBHOOK_SECRET: string }) { if (new URL(req.url).pathname !== '/webhooks/hatched') { return new Response('not found', { status: 404 }); } const result = await verifyNextAppRequest(req, env.HATCHED_WEBHOOK_SECRET); if (!result.valid || !result.event) { return new Response(result.reason ?? 'invalid_signature', { status: 400 }); } // Queue the event onto a Cloudflare Queue / Durable Object // for processing; ack immediately. return new Response(null, { status: 202 }); }, }; ``` ## Outside Node — Python / Go / Ruby The signing scheme is plain HMAC-SHA256. Anything that can read raw bytes and compute a digest works. The [Handle webhooks](/docs/guides/handle-webhooks#manual-verification-without-the-sdk) guide has Python and Go implementations side-by-side with the Node version. ## Idempotency in detail Hatched stamps each delivery with a unique id in the `X-Hatched-Delivery` header. Retries of the same event reuse the original id, so deduplication is a single key lookup on that header value: ```ts async function alreadyHandled(deliveryId: string): Promise { // Any durable store works — Redis SETNX, Postgres unique constraint, KV. const inserted = await redis.set(`webhook:${deliveryId}`, '1', 'NX', 'EX', 86_400); return inserted === null; // null = key existed → already handled } ``` Hold the dedupe record for at least 24 hours; Hatched stops retrying after three failed attempts (+5s, +30s, +5min), but operators can replay manually from the dashboard for longer. ## Replay protection — why the 5-minute window Each delivery carries an `X-Hatched-Timestamp` header that Hatched signs alongside the body (the HMAC is over `` `${timestamp}.${rawBody}` ``). The SDK adapters reject anything older than 300 seconds by default — same as Stripe, Slack, GitHub. This blocks attackers who somehow recorded a real delivery from replaying it later. If a request fails *only* because the timestamp is too old, log the delta between now and `X-Hatched-Timestamp`. A consistent 30-second skew points at clock drift on your host — fix NTP rather than widening the tolerance. ## Rotate the signing secret Every adapter accepts either a single `string` secret or an array of strings. Pass two during a rotation window so the verifier accepts payloads signed under the previous *or* the new secret. The platform side flips to the new secret the moment you call `rotateSecret` — your job is to give every host in your fleet time to pick up the new value without dropping events in between. ```ts import { verifyExpressRequest } from '@hatched/sdk-js'; // During rotation, accept both. After every host has the new secret in // its env, drop the previous one from the list. const secrets = [ process.env.HATCHED_WEBHOOK_SECRET, // new process.env.HATCHED_WEBHOOK_SECRET_PREV, // previous — empty/undefined OK ].filter((s): s is string => !!s); app.post('/webhooks/hatched', express.raw({ type: 'application/json' }), (req, res) => { const { valid, event } = verifyExpressRequest(req, secrets); if (!valid || !event) return res.status(400).end(); // ... }); ``` The recommended sequence: 1. Deploy your handler with `secrets = [old]`. Confirm green. 2. Add the rotation env var: `secrets = [old, new]`. Deploy. Both secrets are now accepted; nothing else has changed yet. 3. Call `client.webhooks.rotateSecret(endpointId)` — the response carries the new plaintext secret. Store it in your secrets manager and ship the `HATCHED_WEBHOOK_SECRET` env update. 4. Once every host has the new value, redeploy with `secrets = [new]`. Drop the previous secret from your secrets manager. Hatched does not maintain a server-side grace window. After step 3, every new delivery is signed with the new secret only. The `secrets = [old, new]` window on the consumer side is what makes the rollout zero-downtime. If you only have a single host, you can compress this to three steps: add the new secret to the array, call `rotateSecret`, redeploy with the new secret alone. ## Test before you ship Dashboard → Developers → Webhook deliveries has a **Send test delivery** button per endpoint. It produces the same signed request shape as live deliveries, so your handler logic is exercised end-to-end. The SDK also exposes a static helper for unit tests: ```ts import { WebhooksResource } from '@hatched/sdk-js'; const ok = WebhooksResource.verifySignature(rawBody, signatureHeader, secret, { timestamp, // value of the X-Hatched-Timestamp header }); expect(ok).toBe(true); ``` Pair that with a fixture rawBody and a known timestamp + secret to keep your test deterministic. --- # Content Security Policy > The minimum CSP directives a partner site needs to host Hatched widgets, plus variations for custom domains and staging hosts. Source: https://docs.hatched.live/docs/guides/widget-csp Hatched widgets load via a ` ``` The loader still emits the Google Fonts `@import` (it is harmless when the overridden families resolve first), but if the import is CSP-blocked the widgets simply use your `--hw-font-*` families with no visual regression. See [Theme tokens → Typography](/docs/reference/theme-tokens#typography) for the full font-token list. --- # Unlock gates > Spend primary tokens to unlock features — the non-cosmetic half of the token economy. Source: https://docs.hatched.live/docs/guides/unlock-gates Unlock gates are how tokens get a meaning **beyond dressing up the buddy**. A gate is a named flag stored against a buddy; the user "unlocks" it by spending primary tokens. Whether it guards a premium feature, a higher difficulty, or a surprise reward is up to you. The primitive is deliberately generic: Hatched stores the unlock, deducts the tokens, and guarantees idempotency. The client decides what the unlock *means*. ## Create a gate Gates are authored in the dashboard under **Settings → Gates**. Each gate has: - `gate_key` — stable identifier (e.g. `advanced_mode`, `custom_skin_2`). Snake_case recommended. - `token_key` — which primary token pays for it. Must match the customer's primary slot. - `cost` — positive integer. - `metadata` (optional) — arbitrary JSON returned to the client on lookup. Put display strings and feature flags here. Gates live at the customer level, not per-buddy — every buddy can unlock the same gate once. ## Unlock at runtime ```ts import { HatchedClient, InsufficientBalanceError } from '@hatched/sdk-js'; const hatched = new HatchedClient({ apiKey: process.env.HATCHED_API_KEY! }); try { const result = await hatched.gates.unlock(buddyId, 'advanced_mode'); if (result.alreadyUnlocked) { // Idempotent — no tokens spent, existing unlock returned. console.log('Already unlocked at', result.unlockedAt); } else { // First unlock — tokens just got deducted. console.log('Unlocked', result.gateKey, '— balance now', result.balanceAfter); } } catch (err) { if (err instanceof InsufficientBalanceError) { // User needs more tokens — show a nudge. } else { throw err; } } ``` The call is **idempotent**: repeat calls return `{ alreadyUnlocked: true }` without touching the ledger. That means you can retry safely on network failures, and you can call `unlock()` optimistically from a UI without double-spending. ## List a buddy's unlocks ```ts const { unlocks } = await hatched.gates.unlocks(buddyId); // { // unlocks: [ // { gateKey: 'advanced_mode', unlockedAt: '2026-04-20T10:00:00Z', metadata: { ... } }, // ], // } ``` Typical usage: fetch once on app load, cache in the client, and treat it as the source of truth for which features to render. ## List available gates ```ts const { gates } = await hatched.gates.list(); // { // gates: [ // { gateKey: 'advanced_mode', tokenKey: 'gems', cost: 50, metadata: { label: 'Advanced mode' } }, // ], // } ``` Use this to render a "shop" of feature unlocks alongside the marketplace. ## Publishable-key access `gates.unlock` is scope-gated. A publishable key needs the `write:unlocks` scope granted explicitly — it is not part of the default scopes. That keeps browser-embedded clients from draining tokens without intent. `gates.unlocks` is read-only and allowed under the default `read:buddies` scope. `gates.list` is secret-key only — a publishable key calling it gets `403 publishable_key_scope`. ## Gotchas - **Primary slot only.** Gates can't spend progression tokens, by design (progression is monotonic). The dashboard refuses a gate pointing at the progression key. - **No undo.** There's no "refund" endpoint for an accidental unlock. Rename the gate key if you want to effectively reset (old unlocks remain attached to the dead key but the client treats them as stale). - **`alreadyUnlocked: true` is normal.** A client calling `unlock()` inside a `useEffect` on mount is a supported pattern — the second call is free. ## Related - [Tokens](/docs/concepts/tokens) — the two-tier model that backs gate costs. - [Token economy](/docs/concepts/token-economy) — how the primary slot fits into spending. - [Marketplace](/docs/concepts/marketplace) — the other primary-spent surface. --- # Next.js integration > Wire Hatched into a Next.js App Router app — server components, route handlers, widgets, and webhooks. Source: https://docs.hatched.live/docs/guides/nextjs-integration Next.js is the most common host for Hatched integrations. The SDK is server-only, so every call happens in a server component, a route handler, or middleware — never in a `"use client"` component. ## 1. Install and configure ```bash pnpm add @hatched/sdk-js ``` ```bash # .env.local HATCHED_API_KEY=hatch_test_... HATCHED_WEBHOOK_SECRET=whsec_... ``` ## 2. Shared client ```ts // lib/hatched.ts import { HatchedClient } from '@hatched/sdk-js'; export const hatched = new HatchedClient({ apiKey: process.env.HATCHED_API_KEY!, }); ``` Importing this module into a client component won't fail the build — the SDK enforces server-only usage at **runtime**: `HatchedClient` throws inside its constructor when it detects a browser (the `assertServerRuntime` guard, which fires when both `window` and `document` are present). Keep it under `lib/` or `server/` so it's never pulled into a `"use client"` bundle. For true build-time protection, add `import 'server-only';` at the top of `lib/hatched.ts` — the SDK itself only guards at runtime. ## 3. Server component reads ```tsx // app/buddy/page.tsx import { hatched } from '@/lib/hatched'; export default async function BuddyPage({ params }: { params: { userId: string } }) { const buddies = await hatched.buddies.list({ userId: params.userId }); return ; } ``` ## 4. Route handlers for writes Mutations (events, coin earn/spend, widget session mint) go through route handlers. They run on the server with access to `HATCHED_API_KEY`. ```ts // app/api/hatched/events/route.ts import { hatched } from '@/lib/hatched'; import { ValidationError } from '@hatched/sdk-js'; export async function POST(req: Request) { const { userId, lessonId, score } = await req.json(); try { const effects = await hatched.events.send({ eventId: `lesson_${lessonId}_${userId}`, userId, type: 'lesson_completed', properties: { lessonId, score }, }); return Response.json(effects); } catch (err) { if (err instanceof ValidationError) { return Response.json({ error: err.details }, { status: 422 }); } throw err; } } ``` ## 5. Widget session mint endpoint Your browser widget calls this to get a short-lived token. Never expose your secret API key directly. ```ts // app/api/hatched/session/route.ts import { hatched } from '@/lib/hatched'; import { getServerSession } from '@/lib/auth'; export async function POST() { const user = await getServerSession(); if (!user) return new Response('unauthorized', { status: 401 }); const session = await hatched.widgetSessions.create({ buddyId: user.buddyId, userId: user.id, scopes: ['read', 'events:track', 'marketplace:browse'], ttlSeconds: 60 * 15, }); return Response.json({ token: session.token, expiresAt: session.expiresAt }); } ``` React does **not** execute a `
``` ## Event was ingested but no effects fired **Signal.** `effects.coins === undefined` or empty; nothing moved. **Why.** No coin rule, badge rule, or skill rule currently matches the `type` you sent. **Fix.** Dashboard → Developers → Event log → click the event → **Evaluation trace** shows which rules were considered and why none fired. Typical mismatches: - Event `type` doesn't match any rule (typo: `lesson_complete` vs `lesson_completed`). - Rule is on draft, not published. - `audience` filter excludes this user. - Buddy is on an older config version that doesn't contain the new rule — migrate via Dashboard → Buddies → Migration. ## Appearance update stuck or needs rerender **Signal.** Marketplace equip/unequip is disabled, the widget shows an appearance banner, or the SDK returns a conflict with `code: 'needs_rerender'`. The buddy response has `appearance.status` as `pending`, `awaiting_credits`, or `failed`. **Why.** Outfit changes and evolution render a new image composite over `base_image_url`. That render may still be queued, waiting for image credits, or blocked because an older buddy image was migrated from a contaminated composite and needs a clean bare stage. **Fix.** - For `pending`, wait for `/widget/state` or `operations.wait(...)` to report completion. - For `awaiting_credits`, add credits or wait for the scheduled retry. - For `failed` with `error.code === 'needs_rerender'`, call `hatched.buddies.rerenderAppearance(buddyId)` or `POST /widget/appearance/rerender`, wait for `ready`, then re-equip the desired items. ## Support Include these four things in every support ticket: - **Request id** from `hatched.getLastRequestId()` or the `X-Request-Id` response header. - SDK version (`@hatched/sdk-js` in your lockfile). - Minimal reproduction — the five lines of code, not the whole file. - What you expected vs what happened. --- # HTTP API > Lean API contract and state machines — V1 scope, endpoints, authentication, and the business processes behind each operation. Source: https://docs.hatched.live/docs/reference/http-api > Most integrations should use [`@hatched/sdk-js`](/docs/reference/sdk-js) > rather than calling these endpoints directly — it handles auth, retries, > idempotency, error parsing, and edge runtimes for you. Reach for raw HTTP > only when there's no SDK for your language. ## Quick reference | | | | --- | --- | | **Base URL (production)** | `https://api.hatched.live/api/v1` | | **Base URL (staging)** | `https://api.staging.hatched.live/api/v1` | | **Auth** | `Authorization: Bearer ` — secret key (`hatch_live_*` / `hatch_test_*`, server-only) or publishable key (`hatch_pk_*`, limited reads). Widgets pass a short-lived session token instead. See [Auth model](/docs/concepts/auth-model). | | **Casing** | The raw HTTP API uses **`snake_case`** for every request and response field (`user_id`, `buddy_id`, `ttl_seconds`). Sending camelCase is rejected (`property userId should not exist`). [`@hatched/sdk-js`](/docs/reference/sdk-js) is the only place you write camelCase — it converts to `snake_case` on the wire and back on responses. SDK code samples (` ```ts `) use camelCase; raw HTTP samples (` ```http ` / curl) use `snake_case`. (The error envelope below is the one camelCase exception — `requestId`, not `request_id`.) | | **First-run flow** | Minting a widget token requires an existing `buddy_id` — you can't go from `user_id` straight to a session token. The full path (published config → reuse-or-create egg → ready → hatch → poll operation → persist `buddy_id` → `POST /widget-sessions`) is in [First user bootstrap](/docs/guides/first-user-bootstrap). | | **Errors** | Always the canonical envelope `{ "error": { "code", "message", "details?", "requestId" } }`. Codes are stable — branch on `code`, not `message`. See [Error codes](/docs/reference/error-codes). | | **Request correlation** | Every request echoes an `X-Request-Id` header; it also appears in the error envelope, your logs, and outgoing webhook payloads. Include it in support requests. | | **Idempotency** | Two separate mechanisms. (1) `POST /events` auto-dedupes on the `event_id` you supply — resending is a header-free no-op. (2) Any mutating write (`POST`/`PUT`/`PATCH`/`DELETE`) accepts an `Idempotency-Key` header: the response is cached for 24h, replaying the same key **and** body returns the cached response with an `Idempotency-Replayed: true` header, and reusing the key with a different body returns `409 idempotency_key_conflict`. The coin / token / purchase-item writes flagged `(supports idempotency)` in the [Endpoints](#endpoints) table are exactly these — set an `Idempotency-Key` before you retry them. Only successful responses are cached, so a failed mutation retries fresh. Still guard `POST /eggs` against React Strict Mode / focus re-runs (see the bootstrap guide), or use `?ensure=true`. | | **Async work** | Image-producing calls (hatch, evolve, equip) return an `operationId`. Poll `GET /operations/{id}` or use `operations.wait(id)` in the SDK. Don't tight-loop. | | **Pagination** | List endpoints return either `{ data, pagination: { nextCursor, hasMore, limit } }` (cursor-based — pass `cursor`) or `{ data, meta: { page, limit, total } }` (page-based — pass `page`). The key present (`pagination` vs `meta`) tells you which. See [Pagination](/docs/concepts/pagination). | | **Rate limits** | Per-key quotas; `429` responses carry `Retry-After` and `X-RateLimit-*` headers. The SDK retries with backoff by default. See [Rate limits](/docs/reference/rate-limits). | | **Billing** | `402` with `code: 'credit_insufficient'`, `event_quota_exceeded`, or `plan_feature_locked` when you hit a billing limit. See [Handling 402](/docs/billing/handling-402). | The rest of this page is the V1 product contract — scope, state machines, and the business processes behind each operation. The full machine-generated endpoint list is in [Endpoints](#endpoints) below. --- **Goal:** Position Hatched as a narrow-scope product that does one thing exceptionally well, rather than an "enterprise does-everything" platform. This document clarifies three things: 1. Which business processes V1 definitively supports 2. Which state machines make the system deterministic 3. How the API contracts stay lean and easy to integrate --- ## 1. Product Philosophy V1 targets for Hatched: - Not a "platform" where the customer designs their own game - A service that reliably produces buddy progression from the customer's existing product events That's why V1 follows these principles: - **Template-first**: limited rule types instead of a free-form rule language - **Publish-before-live**: progression config changes are edited in draft, then published - **Async-by-default**: visual-producing jobs are tracked via operations - **Read vs write separation**: easy for widgets to read, tighter controls on mutations - **Canonical state lives in Hatched**: customers may keep a local copy, but Hatched is the source of truth --- ## 2. V1 Scope ### 2.1 Definitely in V1 - Preset plan selection and customization - Skill set definition - Coin rules - Badge rules - Evolution readiness and evolution trigger - Item marketplace - Buddy widget - Marketplace widget - Event ingestion - Webhook delivery - Multi-buddy support ### 2.2 Not in V1 - Full no-code workflow builder - Unlimited rule engine with if/else trees - Cross-customer shared economy - Buddy-to-buddy social graph - Full user-level CRM - Real-time multiplayer / competition engine - Arbitrary CSS/JS execution on the customer side - Custom approval flows tailored per customer need ### 2.3 Deliberately limited in V1 - Rule types are picked from fixed enums - Widget theme is configurable but not an infinite design surface - Evolution capped at 5 stages - Token types start with a limited set of system tokens - Marketplace visibility and requirement logic ships with predefined operators --- ## 3. User-Friendly Business Processes ### 3.1 Customer onboarding Goal: ship an integration that's live in 10 minutes. Flow: 1. Customer creates an account 2. Picks a preset plan 3. Lightly edits skill, badge, coin and evolution fields 4. Publishes the draft 5. Receives an API key 6. Sends the first `POST /events` request 7. Generates a widget token and embeds it UI principles: - First screen exposes at most 3 major decisions: preset, style, widget theme - "Advanced" fields are collapsed by default in every editor - No "empty page" feeling; the starter config from the preset is always visible ### 3.2 Progression config change Goal: make changes without disturbing existing users' balance. Flow: 1. Customer opens the draft config 2. Edits skill/badge/coin/evolution fields 3. System shows an impact summary: - affects new buddies - does not affect existing buddies - migration can be run separately on demand 4. Customer publishes 5. New `config_version` becomes active UI principles: - "Save" and "Publish" separation must be obvious - Publish modal uses impact language, not technical jargon - "This change does not retroactively affect existing buddies" must be clearly shown ### 3.3 Event-driven reward generation Goal: the customer only sends the event; Hatched computes the reward. Flow: 1. Customer sends the event via `POST /events` 2. Hatched deduplicates the event 3. Rule engine computes effects 4. Writes coin/token/badge/streak/evolution_ready effects 5. Emits a webhook if needed UI principles: - Dashboard shows "recent events" and "generated effects" side by side - Debug screen gives a clear answer to "why didn't this event produce a reward?" ### 3.4 Hatch / equip / evolve Goal: make visual-producing flows deterministic and understandable. Flow: 1. Customer or widget initiates an action 2. API returns an `operation_id` 3. Worker finishes the job 4. Result arrives via webhook or polling UI principles: - "Processing..." is a first-class state - Error messages are action-specific, not generic "Image generation failed" - The user must not retry the same action in a panic ### 3.5 Widget access Goal: easy to integrate, but controlled on the write side. Modes: - **Read-only embed**: buddy widget, leaderboard - **Interactive session**: marketplace purchase, equip item UI principles: - Embed snippet must be short - Token generation should feel like a "copy-paste snippet" experience - A read-only embed should require the minimum possible backend integration --- ## 4. Canonical Domain Concepts ### 4.1 Customer The B2B tenant that uses Hatched. Owns: - plan - settings - active_config_version_id ### 4.2 ConfigVersion Immutable snapshot of progression logic. Contains: - skill set - coin rules - badge definitions - evolution rules - token rules - marketplace requirements ### 4.3 Egg The pending object before a buddy is born. Rules: - bound to a specific user - created with a config_version - transitions to a closed state once hatched ### 4.4 Buddy A user-owned progression unit. Rules: - a user can own multiple buddies - a buddy is pinned to a single config_version - its version does not change unless migration happens ### 4.5 EventIngestion The recorded external domain event from a customer. Rules: - `customer_id + event_id` is unique - the same event does not produce a reward twice ### 4.6 EconomyLedger Immutable ledger of coin/token mutations. Rules: - each row is a credit or a debit - balance is a computed/projection field - mutation endpoints write to the ledger ### 4.7 Operation Record for tracking an asynchronous job. Types: - hatch - equip_item - evolve --- ## 5. State Machines ### 5.1 ConfigVersion State Machine States: - `draft` - `published` - `archived` Transitions: - `draft -> published` - `published -> archived` Rules: - only one `published` version may be active - publish creates a new version; it never mutates an existing one ### 5.2 Egg State Machine States: - `waiting` - `ready` - `hatching` - `hatched` - `cancelled` Transitions: - `waiting -> ready` - `ready -> hatching` - `hatching -> hatched` - `waiting -> cancelled` - `ready -> cancelled` Rules: - `hatch` may only be initiated from `ready` - `hatching` persists until the operation completes ### 5.3 Buddy State Machine The top-level buddy state is kept simple: - `active` - `archived` A buddy's real variables are: - `evolution_stage` - `skills` - `coins` - `tokens` - `equipped_items` Rules: - an attribute-based model is preferred over a progression state machine - this keeps the UI simpler ### 5.4 Operation State Machine States: - `pending` - `processing` - `completed` - `failed` - `cancelled` Transitions: - `pending -> processing` - `processing -> completed` - `processing -> failed` - `pending -> cancelled` Rules: - the client never treats the result as final until it sees `completed/failed` ### 5.5 WidgetSession State Machine States: - `issued` - `active` - `expired` - `revoked` Rules: - read-only tokens may be longer-lived - interactive tokens must be short-lived - interactive tokens operate on scopes --- ## 6. Lean API Contract ### 6.1 Public integration endpoints #### `POST /api/v1/eggs` Purpose: - create a pending egg record for a user Query params: - `ensure=true` — return this user's most recent `waiting`/`ready` egg if one already exists instead of creating a new one (idempotent first-run bootstrap; also dodges the per-user active-egg cap on retries). Request: ```json { "user_id": "user_123", "metadata": { "age": 12, "level": "beginner" } } ``` Response: ```json { "egg_id": "egg_abc", "status": "waiting", "visual_variant": 7, "config_version_id": "cfg_v12", "user_id": "user_123", "buddy_id": null, "metadata": { "age": 12, "level": "beginner" }, "created_at": "2026-04-08T10:30:00Z" } ``` `buddy_id` is `null` until the egg reaches `status: "hatched"`, after which it carries the hatched buddy's id (same on `GET /api/v1/eggs/{egg_id}` and `GET /api/v1/eggs`). Errors: - `409 no_published_config` — the customer has no published config version yet; `details.publish_url` links to the dashboard publish page. - `409 active_egg_limit` — the per-user active-egg cap is reached; `details.active` lists the existing eggs (`egg_id`, `status`, `created_at`) and `details.max` the cap. Hatch/cancel one, or retry with `?ensure=true`. #### `PATCH /api/v1/eggs/{egg_id}/status` Purpose: - mark the egg `ready` or cancel it Allowed statuses: - `ready` - `cancelled` #### `POST /api/v1/eggs/{egg_id}/hatch` Purpose: - kick off an async hatch operation Response: ```json { "accepted": true, "operation_id": "op_123", "status": "pending" } ``` #### `POST /api/v1/events` Purpose: - ingest a customer domain event Request: ```json { "event_id": "evt_789", "user_id": "user_123", "type": "lesson_completed", "occurred_at": "2026-04-08T10:30:00Z", "audience": "students", "properties": { "lesson_id": "lesson_456", "score": 87 } } ``` `audience` is **optional for single-audience customers** (the server applies the implicit default) but **required once you've defined 2+ audiences** — omit it then and the call fails with `400 missing_audience`; send an unknown value and it fails with `400 unknown_audience`. The value must match an audience key configured in Dashboard → Audiences. Response: ```json { "accepted": true, "event_id": "evt_789", "effects": { "coins": 10, "badges_awarded": [], "tokens": [], "evolution_ready": false } } ``` #### `GET /api/v1/buddies/{buddy_id}` Purpose: - return canonical buddy state #### `POST /api/v1/buddies/{buddy_id}/evolve` Purpose: - start an async evolution operation #### `PATCH /api/v1/buddies/{buddy_id}/equipped-items` Purpose: - start an equip/unequip operation #### `GET /api/v1/operations/{operation_id}` Purpose: - read the status of a hatch/equip/evolve ### 6.2 Manual override endpoints These endpoints ship in V1 but are not marketed as the "primary flow": - `PATCH /buddies/{id}/skills` - `POST /buddies/{id}/coins` - `POST /buddies/{id}/coins/spend` - `POST /buddies/{id}/badges` - `POST /buddies/{id}/tokens` Use cases: - admin correction - migration - special campaign - support operation ### 6.3 Widget endpoints #### `POST /api/v1/widget-sessions` Purpose: - mint an interactive widget session token Request: ```json { "buddy_id": "b3d7c8a0-1234-4f5e-9abc-def012345678", "user_id": "user_42", "scopes": ["marketplace:purchase", "items:equip"] } ``` Response: ```json { "token": "wgt_sess_xxx", "session_id": "3d7ec5a4-7c47-41aa-a869-2e63e2d0d3c8", "expires_at": "2026-04-08T11:00:00Z", "scopes": ["marketplace:purchase", "items:equip"] } ``` #### `POST /api/v1/embed-tokens` Purpose: - mint a signed token for a read-only widget --- ## 7. UX Guardrails for the Dashboard ### 7.1 Rule templates instead of a rule builder The V1 panel must offer: - Pick a trigger - Pick a reward - Pick a limit - Preview impact The V1 panel must NOT offer: - nested if/else - custom expression language - event transformation DSL - arbitrary JSON editor as the primary UX ### 7.2 Publish UX Every progression editor follows this shape: - Draft badge - Unsaved changes indicator - Publish CTA - Impact summary - "Publish a new version" instead of a rollback model ### 7.3 Support UX Support/operator screens must surface: - recent events - effects generated from an event - operation status - the user's buddy list - recent ledger activity These screens exist for operational confidence, not enterprise complexity. --- ## 8. Business Process Decisions ### 8.1 What we keep centralized Hatched owns the truth for: - progression truth - config version truth - buddy ownership truth - operation truth - ledger truth ### 8.2 What we leave to the customer The customer owns: - event production - user identity mapping - when a product action emits an event - maintaining the buddy ID list inside their own product UI - which widget appears on which screen ### 8.3 What we deliberately don't build - We do not move the customer's whole business logic into Hatched - We are not a full BPM/workflow product - We are not a CRM or engagement automation hub - We do not build an infinitely configurable admin panel --- ## 9. Recommended V1 Motto **"Customer events in, progression out."** That's the strongest, simplest positioning for the product: - The customer emits events - Hatched computes progression - The widget renders the result Everything else is secondary. ## Endpoints {/* ENDPOINTS_START */} {/* AUTO-GENERATED from apps/api/openapi.public.json by apps/docs/scripts/generate-api-reference.ts. Run `pnpm --filter @hatched/api openapi:dump` first if the artifact is stale. */} | Method | Path | Summary | Tag | |---|---|---|---| | `GET` | `/api/v1/analytics/activity-summary` | Get activity summary | Analytics | | `GET` | `/api/v1/analytics/audiences` | Get audience breakdown | Analytics | | `GET` | `/api/v1/analytics/custom-events` | Get custom event trends | Analytics | | `GET` | `/api/v1/analytics/economy-health` | Get economy health | Analytics | | `GET` | `/api/v1/analytics/economy-summary` | Get economy summary | Analytics | | `GET` | `/api/v1/analytics/engagement` | Get engagement metrics | Analytics | | `GET` | `/api/v1/analytics/event-counts` | Per-event counts | Analytics | | `GET` | `/api/v1/analytics/evolution` | Get evolution timeline | Analytics | | `GET` | `/api/v1/analytics/feature-activity` | Get feature activity | Analytics | | `GET` | `/api/v1/analytics/marketplace-funnel` | Get marketplace funnel | Analytics | | `GET` | `/api/v1/analytics/overview` | Get analytics overview | Analytics | | `GET` | `/api/v1/analytics/popular-badges` | Get popular badges | Analytics | | `GET` | `/api/v1/analytics/popular-items` | Get popular items | Analytics | | `GET` | `/api/v1/analytics/retention` | Get retention metrics | Analytics | | `GET` | `/api/v1/analytics/roi-metrics` | Get ROI metrics | Analytics | | `GET` | `/api/v1/analytics/streaks` | Get streak health | Analytics | | `GET` | `/api/v1/analytics/tokens` | Get per-token economy | Analytics | | `GET` | `/api/v1/analytics/webhooks` | Get webhook delivery health | Analytics | | `GET` | `/api/v1/auth/api-keys` | List all active API keys for the current customer | Auth | | `POST` | `/api/v1/auth/api-keys` | Create a new API key | Auth | | `DELETE` | `/api/v1/auth/api-keys/{id}` | Revoke an API key by its ID | Auth | | `POST` | `/api/v1/auth/api-keys/rotate` | Rotate API keys by revoking all existing keys and creating a new one | Auth | | `GET` | `/api/v1/auth/api-keys/whoami` | Return the identity, plan, capabilities, and scopes of the calling credential | Auth | | `POST` | `/api/v1/auth/email/verification/request` | Request a fresh dashboard email verification link | Auth | | `POST` | `/api/v1/auth/email/verify` | Verify dashboard account email with a one-time token | Auth | | `POST` | `/api/v1/auth/login` | Authenticate and obtain a JWT token | Auth | | `GET` | `/api/v1/auth/me` | Get the currently authenticated customer profile | Auth | | `POST` | `/api/v1/auth/password/change` | Change the current dashboard account password | Auth | | `POST` | `/api/v1/auth/password/reset` | Reset dashboard password with a one-time token | Auth | | `POST` | `/api/v1/auth/password/reset/request` | Request a dashboard password reset token | Auth | | `POST` | `/api/v1/auth/publishable-keys` | Issue a browser-safe publishable key (hatch_pk_*) with a scoped set of permissions. | Auth | | `POST` | `/api/v1/auth/register` | Register a new customer account | Auth | | `GET` | `/api/v1/auth/sso/callback` | Complete dashboard SSO callback from OIDC provider | Auth | | `GET` | `/api/v1/auth/sso/config` | Return public dashboard SSO configuration | Auth | | `GET` | `/api/v1/auth/sso/start` | Start dashboard SSO via generic OIDC | Auth | | `GET` | `/api/v1/badge-definitions` | List all badge definitions | Badge Definitions | | `POST` | `/api/v1/badge-definitions` | Create a badge definition | Badge Definitions | | `DELETE` | `/api/v1/badge-definitions/{id}` | Delete a badge definition | Badge Definitions | | `GET` | `/api/v1/badge-definitions/{id}` | Get a badge definition | Badge Definitions | | `PUT` | `/api/v1/badge-definitions/{id}` | Update a badge definition | Badge Definitions | | `POST` | `/api/v1/badge-definitions/{id}/regenerate-icon` | Queue an AI regeneration for this badge icon | Badge Definitions | | `POST` | `/api/v1/badge-definitions/generate-icon` | Generate a badge icon with AI | Badge Definitions | | `POST` | `/api/v1/badge-definitions/upload-icon` | Upload a badge icon | Badge Definitions | | `POST` | `/api/v1/billing/checkout` | Create checkout session | Billing | | `GET` | `/api/v1/billing/checkout/session/{id}` | Reconcile a Stripe checkout session | Billing | | `POST` | `/api/v1/billing/portal` | Create billing portal session | Billing | | `GET` | `/api/v1/billing/status` | Get billing status | Billing | | `GET` | `/api/v1/buddies` | List buddies with pagination and optional filters | Buddies | | `POST` | `/api/v1/buddies/{buddy_id}/appearance/rerender` | Regenerate the buddy stage base image | Buddies | | `GET` | `/api/v1/buddies/{buddy_id}/badges` | List all badges awarded to a buddy | Buddies | | `POST` | `/api/v1/buddies/{buddy_id}/badges` | Award a badge to a buddy | Buddies | | `POST` | `/api/v1/buddies/{buddy_id}/coins` | Earn coins for a buddy (supports idempotency) | Buddies | | `POST` | `/api/v1/buddies/{buddy_id}/coins/spend` | Spend coins for a buddy (supports idempotency) | Buddies | | `PATCH` | `/api/v1/buddies/{buddy_id}/equipped-items` | Equip or unequip items on a buddy | Buddies | | `GET` | `/api/v1/buddies/{buddy_id}/evolution` | Check evolution readiness and progress for a buddy | Buddies | | `GET` | `/api/v1/buddies/{buddy_id}/evolutions` | Stage transition timeline for a buddy | Buddies | | `POST` | `/api/v1/buddies/{buddy_id}/evolve` | Trigger asynchronous buddy evolution | Buddies | | `POST` | `/api/v1/buddies/{buddy_id}/gates/{gate_key}/unlock` | Spend tokens to unlock a gate for a buddy | Gates | | `GET` | `/api/v1/buddies/{buddy_id}/progression` | Get buddy progression metrics (legacy endpoint) | Buddies | | `GET` | `/api/v1/buddies/{buddy_id}/progression-metrics` | Get buddy progression metrics (lessons, quizzes, streaks, etc.) | Buddies | | `POST` | `/api/v1/buddies/{buddy_id}/purchase-item` | Purchase a marketplace item using coins (supports idempotency) | Buddies | | `GET` | `/api/v1/buddies/{buddy_id}/purchased-items` | List all purchased items for a buddy | Buddies | | `GET` | `/api/v1/buddies/{buddy_id}/tokens` | Typed token balances (primary + progression) | Buddies | | `POST` | `/api/v1/buddies/{buddy_id}/tokens` | Earn or spend tokens for a buddy (supports idempotency) | Buddies | | `POST` | `/api/v1/buddies/{buddy_id}/unlock-item` | Unlock an item without spending coins | Buddies | | `GET` | `/api/v1/buddies/{buddy_id}/unlocks` | List gates this buddy has unlocked | Gates | | `GET` | `/api/v1/buddies/{id}` | Get buddy details with full canonical state | Buddies | | `PATCH` | `/api/v1/buddies/{id}` | Update a buddy name | Buddies | | `PATCH` | `/api/v1/buddies/{id}/archive` | Archive a buddy (one-way transition from active to archived) | Buddies | | `POST` | `/api/v1/buddies/{id}/share` | Mint (or fetch) the public share link for a buddy | Buddies | | `POST` | `/api/v1/buddies/{id}/share/events` | Record a share-sheet funnel event (opened / shared) | Buddies | | `PATCH` | `/api/v1/buddies/{id}/skills` | Update buddy skill levels (increase, decrease, or set) | Buddies | | `GET` | `/api/v1/buddies/users/{user_id}/summary` | Get a user summary including buddy count, coins, and badges | Buddies | | `GET` | `/api/v1/buddy-share/settings` | Tenant buddy-share settings (toggles + resolved link origin) | Buddies | | `PATCH` | `/api/v1/buddy-share/settings` | Update buddy-share toggles (enabled / show_tenant_name / cta_url) | Buddies | | `GET` | `/api/v1/buddy-share/stats` | Rolling-window buddy-share funnel for the tenant | Buddies | | `GET` | `/api/v1/coin-rules` | List all coin rules | Coin Rules | | `POST` | `/api/v1/coin-rules` | Create a coin rule | Coin Rules | | `DELETE` | `/api/v1/coin-rules/{id}` | Delete a coin rule | Coin Rules | | `PUT` | `/api/v1/coin-rules/{id}` | Update a coin rule | Coin Rules | | `GET` | `/api/v1/coin-rules/{id}/reward-pool/telemetry` | Reward pool telemetry | Coin Rules | | `GET` | `/api/v1/config-versions` | List config versions | Config Versions | | `POST` | `/api/v1/config-versions` | Create or open the draft config version | Config Versions | | `GET` | `/api/v1/config-versions/{id}` | Get config version | Config Versions | | `PATCH` | `/api/v1/config-versions/{id}` | Update config version | Config Versions | | `POST` | `/api/v1/config-versions/{id}/clone` | Clone config version | Config Versions | | `GET` | `/api/v1/config-versions/{id}/impact` | Preview config impact | Config Versions | | `POST` | `/api/v1/config-versions/{id}/migrate-buddies` | Migrate buddies | Config Versions | | `POST` | `/api/v1/config-versions/{id}/publish` | Publish config version | Config Versions | | `GET` | `/api/v1/credits/balance` | Get credit balance | Credits | | `GET` | `/api/v1/credits/ledger` | List recent AI usage ledger entries | Credits | | `GET` | `/api/v1/customers/me` | | Customers | | `PATCH` | `/api/v1/customers/me` | | Customers | | `GET` | `/api/v1/customers/me/analytics/share-funnel` | Public share-page gift funnel (gift CTA + signup wall) | Analytics | | `POST` | `/api/v1/customers/me/apply-preset` | Apply preset | Presets | | `POST` | `/api/v1/customers/me/assets/regenerate` | Bulk regenerate AI assets | Customers | | `PATCH` | `/api/v1/customers/me/audiences` | Replace the customer audience list | Customers | | `GET` | `/api/v1/customers/me/awards` | Customer-wide HR award audit log | Customers | | `GET` | `/api/v1/customers/me/beginners-luck/analytics` | Beginner's Luck winner analytics | Customers | | `POST` | `/api/v1/customers/me/boosters/grant` | Grant a catalog booster to a buddy (admin one-off) | Customers | | `GET` | `/api/v1/customers/me/brag/by-channel` | Channel × event_kind click / completion matrix | Customers | | `GET` | `/api/v1/customers/me/brag/config` | Get the Brag Button channel + copy-template config | Customers | | `PATCH` | `/api/v1/customers/me/brag/config` | Update channel toggles, copy templates and webhook URLs | Customers | | `GET` | `/api/v1/customers/me/brag/funnel` | Brag funnel aggregate over a date window | Customers | | `GET` | `/api/v1/customers/me/brag/telemetry.csv` | Export raw brag telemetry as CSV | Customers | | `POST` | `/api/v1/customers/me/brag/webhook-test` | Send a dummy message to a Slack/Teams webhook URL | Customers | | `GET` | `/api/v1/customers/me/causes` | List the tenant Cause Counter definitions | Customers | | `POST` | `/api/v1/customers/me/causes` | Create a Cause Counter definition | Customers | | `DELETE` | `/api/v1/customers/me/causes/{id}` | Delete a Cause Counter definition | Customers | | `PATCH` | `/api/v1/customers/me/causes/{id}` | Update a Cause Counter definition | Customers | | `POST` | `/api/v1/customers/me/causes/{id}/draft` | Stage draft edits for a published Cause Counter definition | Customers | | `GET` | `/api/v1/customers/me/causes/{id}/preview-30-days` | Project symbolic units from the last 30 days of eligible events for a saved cause | Customers | | `GET` | `/api/v1/customers/me/causes/{id}/webhook` | HTCH-106 — F4.5 cause webhook config + recent delivery attempts | Customers | | `PATCH` | `/api/v1/customers/me/causes/{id}/webhook` | HTCH-106 — F4.5 set the cause webhook URL, secret and threshold step | Customers | | `POST` | `/api/v1/customers/me/causes/{id}/webhook/test` | HTCH-106 — F4.5 send a test cause.threshold_reached event and return the delivery outcome inline | Customers | | `GET` | `/api/v1/customers/me/causes/analytics` | HTCH-107 — F4.5 Humanity Hero admin analytics: customer-wide and per-team contribution rollups, time series, threshold ETA and webhook delivery health (Planner drawer "Analytics" tab) | Customers | | `GET` | `/api/v1/customers/me/causes/analytics.csv` | HTCH-107 — download the cause analytics as a CSV attachment | Customers | | `GET` | `/api/v1/customers/me/causes/audit` | HTCH-71 — paginated Cause Counter change history (drawer) | Customers | | `GET` | `/api/v1/customers/me/causes/preview-30-days` | HTCH-71 — project symbolic units for an unsaved rate config (the drawer rate builder simulation) | Customers | | `DELETE` | `/api/v1/customers/me/council/narrative/slots/{slot}` | Retire the live proposal in a slot and restore the default copy | Customers | | `PUT` | `/api/v1/customers/me/council/narrative/slots/{slot}` | Promote an approved proposal into a live narrative slot | Customers | | `GET` | `/api/v1/customers/me/council/proposals` | The Council narrative-proposal moderation queue | Customers | | `PATCH` | `/api/v1/customers/me/council/proposals/{id}` | Approve or reject a pending proposal | Customers | | `GET` | `/api/v1/customers/me/event-badges` | List event-triggered badge campaigns | Customers | | `POST` | `/api/v1/customers/me/event-badges` | Create an event-triggered badge campaign | Customers | | `DELETE` | `/api/v1/customers/me/event-badges/{id}` | Delete an event-triggered badge campaign | Customers | | `PATCH` | `/api/v1/customers/me/event-badges/{id}` | Update an event-triggered badge campaign | Customers | | `GET` | `/api/v1/customers/me/feature-config` | Get the tenant feature_config blob | Customers | | `PATCH` | `/api/v1/customers/me/feature-config/{feature_key}` | Update one feature_config block | Customers | | `GET` | `/api/v1/customers/me/feature-toggles` | Get the tenant feature toggle state | Customers | | `PATCH` | `/api/v1/customers/me/feature-toggles` | Update tenant feature toggles | Customers | | `DELETE` | `/api/v1/customers/me/feature-toggles/draft` | Discard the pending draft toggle map | Customers | | `POST` | `/api/v1/customers/me/feature-toggles/publish` | Publish the pending draft toggle map | Customers | | `GET` | `/api/v1/customers/me/flash-sales` | List flash sales for the Planner drawer | Customers | | `POST` | `/api/v1/customers/me/flash-sales` | Schedule a flash sale | Customers | | `DELETE` | `/api/v1/customers/me/flash-sales/{id}` | Cancel a flash sale — a running sale clears its discounts | Customers | | `GET` | `/api/v1/customers/me/founding-cohort/audit` | Paginated Founding Cohort assignment history | Customers | | `GET` | `/api/v1/customers/me/founding-cohort/audit/export.csv` | Export the full Founding Cohort assignment history as CSV | Customers | | `POST` | `/api/v1/customers/me/founding-cohort/backfill` | Retroactively mark every currently-eligible buddy (idempotent) | Customers | | `GET` | `/api/v1/customers/me/founding-cohort/preview` | Project how many buddies the Founding Cohort config would mark. Optional query params preview an unsaved mode/threshold. | Customers | | `GET` | `/api/v1/customers/me/group-quests` | List the tenant Group Quests (filter by status / team) | Customers | | `POST` | `/api/v1/customers/me/group-quests` | Create a Group Quest (status: draft) | Customers | | `DELETE` | `/api/v1/customers/me/group-quests/{questId}` | Delete a Group Quest (draft / cancelled only) | Customers | | `PATCH` | `/api/v1/customers/me/group-quests/{questId}` | Update a Group Quest — draft fields, active deadline-extension, or cancel | Customers | | `POST` | `/api/v1/customers/me/group-quests/{questId}/force-resolve` | HTCH-56 — manually resolve a Group Quest now (admin watchdog override) | Customers | | `POST` | `/api/v1/customers/me/group-quests/{questId}/publish` | Publish a draft Group Quest (draft → active) | Customers | | `GET` | `/api/v1/customers/me/hosted-surfaces` | List the customer’s hosted surfaces | Customers | | `POST` | `/api/v1/customers/me/hosted-surfaces` | Create a hosted surface from a template | Customers | | `GET` | `/api/v1/customers/me/hosted-surfaces/{id}` | Fetch one hosted surface (admin lens) | Customers | | `PATCH` | `/api/v1/customers/me/hosted-surfaces/{id}` | Update name / theme / layout / mode / widget version | Customers | | `POST` | `/api/v1/customers/me/hosted-surfaces/{id}/archive` | | Customers | | `POST` | `/api/v1/customers/me/hosted-surfaces/{id}/logo` | Upload a hosted surface logo and attach it to the public shell theme | Customers | | `GET` | `/api/v1/customers/me/hosted-surfaces/{id}/players` | | Customers | | `POST` | `/api/v1/customers/me/hosted-surfaces/{id}/players` | Add a player. Provide buddy_id to link an existing buddy or display_name to mint a new one. | Customers | | `PATCH` | `/api/v1/customers/me/hosted-surfaces/{id}/players/{playerId}` | | Customers | | `GET` | `/api/v1/customers/me/hosted-surfaces/{id}/players/{playerId}/access-code` | Re-view a player’s current access code + QR token without rotating them. Returns available:false for players created before encrypted-at-rest storage existed — regenerate once to mint a re-viewable copy. Audited. | Customers | | `POST` | `/api/v1/customers/me/hosted-surfaces/{id}/players/{playerId}/regenerate-access` | | Customers | | `POST` | `/api/v1/customers/me/hosted-surfaces/{id}/publish` | | Customers | | `GET` | `/api/v1/customers/me/hosted-surfaces/{id}/readiness` | Per-widget content readiness for the go-live checklist | Customers | | `GET` | `/api/v1/customers/me/hosted-surfaces/{id}/recipes` | | Customers | | `POST` | `/api/v1/customers/me/hosted-surfaces/{id}/recipes` | | Customers | | `POST` | `/api/v1/customers/me/hosted-surfaces/{id}/recipes/{key}/run` | | Customers | | `POST` | `/api/v1/customers/me/hosted-surfaces/{id}/unpublish` | | Customers | | `GET` | `/api/v1/customers/me/kudo-types` | List the effective kudo taxonomy | Customers | | `POST` | `/api/v1/customers/me/kudo-types` | Create a custom kudo type | Customers | | `DELETE` | `/api/v1/customers/me/kudo-types/{id}` | Archive a kudo type (soft delete) | Customers | | `PATCH` | `/api/v1/customers/me/kudo-types/{id}` | Update a kudo type | Customers | | `POST` | `/api/v1/customers/me/kudo-types/apply-template` | Apply an industry preset taxonomy | Customers | | `POST` | `/api/v1/customers/me/kudo-types/apply-theme-template` | Apply a theme-aware kudos pack (HTCH-128) | Customers | | `PATCH` | `/api/v1/customers/me/kudo-types/reorder` | Persist a new display order | Customers | | `GET` | `/api/v1/customers/me/leaderboard-config` | Get the tenant leaderboard view-mode config | Customers | | `PATCH` | `/api/v1/customers/me/leaderboard-config` | Update the tenant leaderboard view-mode config | Customers | | `GET` | `/api/v1/customers/me/leagues/config` | Full tier ladder, cohort/cadence config and season state | Customers | | `PATCH` | `/api/v1/customers/me/leagues/config` | Update the cohort maths, season cadence and off-season window | Customers | | `POST` | `/api/v1/customers/me/leagues/seasons` | Schedule the next upcoming season | Customers | | `POST` | `/api/v1/customers/me/leagues/seasons/{seasonId}/force-close` | Manually trigger the rollover for a season (audit logged) | Customers | | `POST` | `/api/v1/customers/me/leagues/seasons/preview` | Project the next three season windows (no write) | Customers | | `PUT` | `/api/v1/customers/me/leagues/tiers` | Bulk-replace the tier ladder — 409 if a removed tier still has buddies | Customers | | `GET` | `/api/v1/customers/me/lotteries` | List lottery definitions for the Planner | Customers | | `POST` | `/api/v1/customers/me/lotteries` | Create a lottery definition | Customers | | `DELETE` | `/api/v1/customers/me/lotteries/{id}` | Soft-delete a lottery (history stays queryable) | Customers | | `PATCH` | `/api/v1/customers/me/lotteries/{id}` | Update a lottery definition | Customers | | `GET` | `/api/v1/customers/me/lotteries/{id}/draws` | Past draw history + analytics for a lottery | Customers | | `GET` | `/api/v1/customers/me/lotteries/{id}/preview-next-draw` | Current-period entry count + next draw time for the preview card | Customers | | `POST` | `/api/v1/customers/me/lotteries/{id}/simulate-draw` | Simulate a draw with the current entries — no rewards granted | Customers | | `GET` | `/api/v1/customers/me/mentor-visibility/config` | Get the mentor-visibility config | Customers | | `PATCH` | `/api/v1/customers/me/mentor-visibility/config` | Update the mentor-visibility config | Customers | | `GET` | `/api/v1/customers/me/mentor-visibility/directory` | List every active mentor across the tenant’s teams | Customers | | `DELETE` | `/api/v1/customers/me/mentor-visibility/sessions` | Reset all mentor session logs for the workspace | Customers | | `GET` | `/api/v1/customers/me/mission-anchor-config` | Get the Mission Anchor admin config | Customers | | `PATCH` | `/api/v1/customers/me/mission-anchor-config` | Update the Mission Anchor admin config | Customers | | `GET` | `/api/v1/customers/me/narrative` | Get the tenant narrative state | Customers | | `PATCH` | `/api/v1/customers/me/narrative` | Update the tenant narrative | Customers | | `GET` | `/api/v1/customers/me/narrative/audit` | List narrative copy change history | Customers | | `GET` | `/api/v1/customers/me/octalysis-state` | Get the tenant Octalysis aggregate state | Customers | | `GET` | `/api/v1/customers/me/onboarding/six-d` | Get the tenant 6D wizard state | Customers | | `PATCH` | `/api/v1/customers/me/onboarding/six-d` | Patch one or more 6D wizard sections | Customers | | `POST` | `/api/v1/customers/me/onboarding/six-d` | Apply the full 6D wizard payload | Customers | | `GET` | `/api/v1/customers/me/onboarding/six-d/audit` | HTCH-137 — Audit timeline | Customers | | `GET` | `/api/v1/customers/me/onboarding/six-d/drift-stats` | HTCH-137 — Config drift stats | Customers | | `POST` | `/api/v1/customers/me/onboarding/six-d/skip` | HTCH-137 — Expert skip | Customers | | `POST` | `/api/v1/customers/me/players/{buddyId}/award` | HR Award Drawer — grant a badge / skill_event / coin / kudo / forced evolution to a buddy | Customers | | `GET` | `/api/v1/customers/me/players/{buddyId}/awards` | Recent HR awards for a buddy (audit lens) | Customers | | `GET` | `/api/v1/customers/me/profile-templates` | List profile-page templates (system + custom) | Customers | | `POST` | `/api/v1/customers/me/profile-templates` | Create a profile-page template | Customers | | `DELETE` | `/api/v1/customers/me/profile-templates/{id}` | Delete a profile-page template | Customers | | `PATCH` | `/api/v1/customers/me/profile-templates/{id}` | Update a profile-page template | Customers | | `POST` | `/api/v1/customers/me/profile-templates/apply-bulk` | Assign a template to many buddies in one statement | Customers | | `GET` | `/api/v1/customers/me/referral` | Get the current workspace referral link | Customers | | `PATCH` | `/api/v1/customers/me/settings` | | Customers | | `GET` | `/api/v1/customers/me/showrooms` | List the customer’s Showroom pages | Customers | | `POST` | `/api/v1/customers/me/showrooms` | Create a Showroom page from a template | Customers | | `GET` | `/api/v1/customers/me/showrooms/{id}` | Fetch one Showroom page (admin lens) | Customers | | `PATCH` | `/api/v1/customers/me/showrooms/{id}` | Update layout / header / visibility | Customers | | `POST` | `/api/v1/customers/me/showrooms/{id}/archive` | Archive a Showroom (hidden from list, kept for audit) | Customers | | `POST` | `/api/v1/customers/me/showrooms/{id}/publish` | Publish a Showroom (status → published) | Customers | | `POST` | `/api/v1/customers/me/showrooms/{id}/regenerate-qr` | Rotate the QR token, invalidating any printed code | Customers | | `POST` | `/api/v1/customers/me/showrooms/{id}/unpublish` | Unpublish a Showroom (status → draft) | Customers | | `GET` | `/api/v1/customers/me/streak-at-risk/analytics` | Streak-at-risk volume + recovery analytics for the Planner drawer | Customers | | `GET` | `/api/v1/customers/me/surprise-drops` | List surprise-drop definitions for the Planner | Customers | | `POST` | `/api/v1/customers/me/surprise-drops` | Create a custom surprise drop | Customers | | `DELETE` | `/api/v1/customers/me/surprise-drops/{id}` | Delete a custom surprise drop | Customers | | `PATCH` | `/api/v1/customers/me/surprise-drops/{id}` | Update a surprise drop — global templates edit copy-on-write | Customers | | `GET` | `/api/v1/customers/me/teams` | List the tenant teams with member counts | Customers | | `POST` | `/api/v1/customers/me/teams` | Create a team | Customers | | `DELETE` | `/api/v1/customers/me/teams/{teamId}` | Soft-delete a team and archive its memberships | Customers | | `PATCH` | `/api/v1/customers/me/teams/{teamId}` | Update a team | Customers | | `GET` | `/api/v1/customers/me/teams/{teamId}/members` | List the active members of a team | Customers | | `POST` | `/api/v1/customers/me/teams/{teamId}/members` | Add a buddy to a team | Customers | | `DELETE` | `/api/v1/customers/me/teams/{teamId}/members/{buddyId}` | Remove a buddy from a team (soft leave) | Customers | | `PATCH` | `/api/v1/customers/me/teams/{teamId}/members/{buddyId}` | Change a member role | Customers | | `POST` | `/api/v1/customers/me/teams/import` | Bulk-import team memberships from a CSV | Customers | | `DELETE` | `/api/v1/customers/me/users/{user_id}/data` | | Customers | | `GET` | `/api/v1/customers/me/users/{user_id}/summary` | | Customers | | `GET` | `/api/v1/customers/me/vacation/analytics` | Vacation usage analytics for the Planner drawer panel | Customers | | `POST` | `/api/v1/customers/me/widget-theme/suggest` | Suggest widget theme customization | Customers | | `GET` | `/api/v1/economy/buddies/{buddyId}/ledger` | Get coin ledger for a buddy | Economy | | `GET` | `/api/v1/eggs` | List eggs with optional user and status filters | Eggs | | `POST` | `/api/v1/eggs` | Create a new egg for a user | Eggs | | `GET` | `/api/v1/eggs/{id}` | Get an egg by its ID | Eggs | | `POST` | `/api/v1/eggs/{id}/hatch` | Start the asynchronous hatch process for an egg | Eggs | | `PATCH` | `/api/v1/eggs/{id}/status` | Update an egg status to ready or cancelled | Eggs | | `POST` | `/api/v1/embed-tokens` | Create embed token | Widget Sessions | | `GET` | `/api/v1/event-types` | List event types | Event Types | | `POST` | `/api/v1/event-types` | Register an event type | Event Types | | `DELETE` | `/api/v1/event-types/{id}` | Delete an event type | Event Types | | `GET` | `/api/v1/event-types/{id}` | Get an event type | Event Types | | `PUT` | `/api/v1/event-types/{id}` | Update or rename an event type | Event Types | | `GET` | `/api/v1/events` | List events | Events | | `POST` | `/api/v1/events` | Ingest event | Events | | `GET` | `/api/v1/events/{id}` | Get event | Events | | `GET` | `/api/v1/events/active-users` | List most-active users in a recent window | Events | | `POST` | `/api/v1/events/admin-trigger` | Trigger an event from the dashboard admin tools | Events | | `POST` | `/api/v1/events/batch` | Ingest event batch | Events | | `GET` | `/api/v1/events/types` | List distinct event types | Events | | `GET` | `/api/v1/gates` | List token gates for this customer | Gates | | `DELETE` | `/api/v1/gates/{gate_key}` | Delete a token gate | Gates | | `PUT` | `/api/v1/gates/{gate_key}` | Create or update a token gate | Gates | | `GET` | `/api/v1/health` | Health check | Health | | `GET` | `/api/v1/health/live` | Liveness check | Health | | `GET` | `/api/v1/health/ready` | Readiness check | Health | | `GET` | `/api/v1/health/version` | Build metadata | Health | | `GET` | `/api/v1/image-usage` | Get image usage | Image Generation | | `GET` | `/api/v1/image-usage/report` | Get image usage report | Image Generation | | `POST` | `/api/v1/marketing/cta` | Record a public marketing CTA click | Marketing | | `GET` | `/api/v1/marketplaces` | List marketplaces | Marketplace | | `POST` | `/api/v1/marketplaces` | Create marketplace | Marketplace | | `GET` | `/api/v1/marketplaces/{id}` | Get marketplace | Marketplace | | `PUT` | `/api/v1/marketplaces/{id}` | Update marketplace | Marketplace | | `GET` | `/api/v1/marketplaces/{id}/items` | List items | Marketplace | | `POST` | `/api/v1/marketplaces/{id}/items` | Create item | Marketplace | | `DELETE` | `/api/v1/marketplaces/{id}/items/{item_id}` | Delete item | Marketplace | | `GET` | `/api/v1/marketplaces/{id}/items/{item_id}` | Get item | Marketplace | | `PUT` | `/api/v1/marketplaces/{id}/items/{item_id}` | Update item | Marketplace | | `POST` | `/api/v1/marketplaces/{id}/items/{item_id}/regenerate-image` | Queue an AI regeneration for this item image | Marketplace | | `POST` | `/api/v1/marketplaces/{id}/items/{item_id}/upload-image` | Upload item image | Marketplace | | `POST` | `/api/v1/marketplaces/{id}/items/import` | Import items | Marketplace | | `POST` | `/api/v1/marketplaces/{id}/items/reorder` | Reorder items | Marketplace | | `POST` | `/api/v1/onboarding/sessions` | Create or resume the current onboarding session | Onboarding | | `PUT` | `/api/v1/onboarding/sessions/{id}/answers` | Patch structured onboarding answers | Onboarding | | `POST` | `/api/v1/onboarding/sessions/{id}/apply` | Apply the generated plan to the customer (writes gamification config) | Onboarding | | `POST` | `/api/v1/onboarding/sessions/{id}/generate-guide` | Generate a personalized integration guide | Onboarding | | `POST` | `/api/v1/onboarding/sessions/{id}/generate-plan` | Generate a gamification plan from the conversation | Onboarding | | `POST` | `/api/v1/onboarding/sessions/{id}/message` | Send a user message and stream the assistant reply via server-sent events | Onboarding | | `POST` | `/api/v1/onboarding/sessions/{id}/regenerate-plan` | Regenerate the plan with a variant seed | Onboarding | | `GET` | `/api/v1/onboarding/sessions/current` | Fetch the current onboarding session | Onboarding | | `GET` | `/api/v1/onboarding/sessions/preparing-status` | Aggregate asset-generation status for the current customer | Onboarding | | `POST` | `/api/v1/onboarding/sessions/reset` | Reset the current onboarding session | Onboarding | | `POST` | `/api/v1/onboarding/sessions/seed-from-description` | Seed onboarding from operator-provided chips + optional description | Onboarding | | `POST` | `/api/v1/onboarding/sessions/seed-from-repo` | Seed onboarding from a repo-analysis brief produced by a local AI agent | Onboarding | | `POST` | `/api/v1/onboarding/sessions/seed-from-url` | Seed onboarding from a landing-page URL | Onboarding | | `POST` | `/api/v1/onboarding/sessions/waitlist` | Join the waitlist for an upcoming onboarding channel | Onboarding | | `GET` | `/api/v1/operations` | List operations with optional type and status filters | Operations | | `GET` | `/api/v1/operations/{id}` | Get an operation by its ID | Operations | | `POST` | `/api/v1/operations/{id}/cancel` | Cancel a pending or processing operation | Operations | | `GET` | `/api/v1/p/{code}` | Resolve a share code to the rich Profile Page v1 payload | Public Share | | `GET` | `/api/v1/path-definitions` | List path definitions | Path Definitions | | `POST` | `/api/v1/path-definitions` | Create a path definition | Path Definitions | | `DELETE` | `/api/v1/path-definitions/{id}` | Delete a path definition | Path Definitions | | `GET` | `/api/v1/path-definitions/{id}` | Get a path definition (with steps + sub-steps) | Path Definitions | | `PUT` | `/api/v1/path-definitions/{id}` | Update a path definition | Path Definitions | | `POST` | `/api/v1/path-definitions/{id}/activate` | Activate a path (atomic single-active per audience) | Path Definitions | | `POST` | `/api/v1/path-definitions/{id}/deactivate` | Deactivate a path | Path Definitions | | `GET` | `/api/v1/path-definitions/{id}/steps` | List steps in a path | Path Definitions | | `POST` | `/api/v1/path-definitions/{id}/steps` | Create a step in a path | Path Definitions | | `DELETE` | `/api/v1/path-definitions/{id}/steps/{stepId}` | Delete a step | Path Definitions | | `PUT` | `/api/v1/path-definitions/{id}/steps/{stepId}` | Update a step | Path Definitions | | `GET` | `/api/v1/path-definitions/{id}/steps/{stepId}/sub-steps` | List sub-steps within a step | Path Definitions | | `POST` | `/api/v1/path-definitions/{id}/steps/{stepId}/sub-steps` | Create a sub-step | Path Definitions | | `DELETE` | `/api/v1/path-definitions/{id}/steps/{stepId}/sub-steps/{subStepId}` | Delete a sub-step | Path Definitions | | `PUT` | `/api/v1/path-definitions/{id}/steps/{stepId}/sub-steps/{subStepId}` | Update a sub-step | Path Definitions | | `PUT` | `/api/v1/path-definitions/{id}/steps/{stepId}/sub-steps/reorder` | Reorder sub-steps within a step | Path Definitions | | `PUT` | `/api/v1/path-definitions/{id}/steps/reorder` | Reorder steps in a path | Path Definitions | | `GET` | `/api/v1/path-definitions/buddies/{buddyId}/paths/{pathKey}` | Get path runtime payload for a buddy | Path Definitions | | `POST` | `/api/v1/path-definitions/buddies/{buddyId}/paths/{pathKey}/sub-steps/{subKey}/complete` | Manually mark a sub-step complete (admin) | Path Definitions | | `GET` | `/api/v1/players/zero` | Read Player Zero status without provisioning it | Widget Sessions | | `POST` | `/api/v1/players/zero` | Create or get the workspace demo player (Player Zero) | Widget Sessions | | `GET` | `/api/v1/presets` | List presets | Presets | | `GET` | `/api/v1/presets/{key}` | Get preset | Presets | | `GET` | `/api/v1/public/b/{code}` | Resolve a buddy share code to its public card data | Public Share | | `POST` | `/api/v1/public/b/{code}/events` | Record a share-page funnel event (viewed / cta_clicked) | Public Share | | `GET` | `/api/v1/public/hall-of-fame-index` | List public Hall of Fame season URLs (paged) | Public Hall of Fame | | `GET` | `/api/v1/public/hall-of-fame/{tenantSlug}` | List a tenant's finalized Hall of Fame seasons | Public Hall of Fame | | `GET` | `/api/v1/public/hall-of-fame/{tenantSlug}/{seasonId}` | One finalized season in the public Hall of Fame | Public Hall of Fame | | `GET` | `/api/v1/public/hosted-surfaces/{slug}` | Resolve a hosted surface slug to its public config (theme, layout, loader URL, auth requirement). | Public Share | | `POST` | `/api/v1/public/hosted-surfaces/{slug}/session` | Exchange an access code or QR token for a short-lived widget session token. | Public Share | | `GET` | `/api/v1/public/returning-champion/welcome-back` | Resolve a Returning Champion welcome-back token to a widget session | Public Returning Champion | | `GET` | `/api/v1/public/share-index` | List public share codes eligible for the sitemap (paged) | Public Share | | `GET` | `/api/v1/public/showroom/{slug}` | Resolve a Showroom slug to its public view | Public Share | | `GET` | `/api/v1/public/showroom/{slug}/qr` | Return the QR payload for a Showroom (url + token). PNG rendering is client-side in v1. | Public Share | | `GET` | `/api/v1/skill-decay-rules` | List skill decay rules | Skill Decay Rules | | `POST` | `/api/v1/skill-decay-rules` | Create a skill decay rule | Skill Decay Rules | | `DELETE` | `/api/v1/skill-decay-rules/{id}` | Delete a skill decay rule | Skill Decay Rules | | `PUT` | `/api/v1/skill-decay-rules/{id}` | Update a skill decay rule | Skill Decay Rules | | `GET` | `/api/v1/skill-decay-rules/{id}/history` | Recent decay applications for a rule | Skill Decay Rules | | `GET` | `/api/v1/skill-decay-rules/{id}/preview` | Preview the cumulative effect of a decay rule | Skill Decay Rules | | `POST` | `/api/v1/skill-decay-rules/run-now` | Trigger a decay sweep immediately for this customer | Skill Decay Rules | | `GET` | `/api/v1/skill-rules` | List all skill rules | Skill Rules | | `POST` | `/api/v1/skill-rules` | Create a skill rule | Skill Rules | | `DELETE` | `/api/v1/skill-rules/{id}` | Delete a skill rule | Skill Rules | | `PUT` | `/api/v1/skill-rules/{id}` | Update a skill rule | Skill Rules | | `POST` | `/api/v1/skill-rules/apply-theme-pack` | Apply a theme-aware skill-rule pack (HTCH-128) | Skill Rules | | `GET` | `/api/v1/skill-sets` | List all skill sets | Skill Sets | | `POST` | `/api/v1/skill-sets` | Create a skill set | Skill Sets | | `DELETE` | `/api/v1/skill-sets/{id}` | Delete a skill set | Skill Sets | | `GET` | `/api/v1/skill-sets/{id}` | Get a skill set | Skill Sets | | `PUT` | `/api/v1/skill-sets/{id}` | Update a skill set | Skill Sets | | `POST` | `/api/v1/skill-sets/generate-icon` | Generate a skill icon with AI | Skill Sets | | `GET` | `/api/v1/stage-assets` | List per-customer stage assets (preset mode buddy art) plus the default library URLs resolved for the customer's creature_style. | Stage Assets | | `DELETE` | `/api/v1/stage-assets/{stage}` | Remove the preset asset for a stage | Stage Assets | | `PUT` | `/api/v1/stage-assets/{stage}` | Commit an uploaded object as the preset asset for a stage | Stage Assets | | `POST` | `/api/v1/stage-assets/{stage}/regenerate` | Queue AI generation for a customer preset stage asset | Stage Assets | | `POST` | `/api/v1/stage-assets/upload-url` | Issue a presigned PUT URL for a client-side stage asset upload | Stage Assets | | `GET` | `/api/v1/streak-definitions` | List all streak definitions | Streak Definitions | | `POST` | `/api/v1/streak-definitions` | Create a streak definition | Streak Definitions | | `DELETE` | `/api/v1/streak-definitions/{id}` | Delete a streak definition | Streak Definitions | | `GET` | `/api/v1/streak-definitions/{id}` | Get a streak definition | Streak Definitions | | `PUT` | `/api/v1/streak-definitions/{id}` | Update a streak definition | Streak Definitions | | `GET` | `/api/v1/token-config` | List token configurations | Token Config | | `POST` | `/api/v1/token-config` | Upsert token configuration | Token Config | | `GET` | `/api/v1/webhook-configs` | List webhook configs | Webhooks | | `POST` | `/api/v1/webhook-configs` | Create webhook config | Webhooks | | `DELETE` | `/api/v1/webhook-configs/{id}` | Delete webhook config | Webhooks | | `GET` | `/api/v1/webhook-configs/{id}` | Get webhook config | Webhooks | | `PUT` | `/api/v1/webhook-configs/{id}` | Update webhook config | Webhooks | | `GET` | `/api/v1/webhook-configs/{id}/deliveries` | List webhook deliveries | Webhooks | | `POST` | `/api/v1/webhook-configs/{id}/deliveries/{deliveryId}/redeliver` | Redeliver webhook | Webhooks | | `POST` | `/api/v1/webhook-configs/{id}/rotate-secret` | Rotate webhook secret | Webhooks | | `POST` | `/api/v1/webhook-configs/{id}/test` | Send test webhook | Webhooks | | `GET` | `/api/v1/webhook-configs/events` | List webhook event types | Webhooks | | `GET` | `/api/v1/webhook-configs/health` | Get webhook delivery health | Webhooks | | `POST` | `/api/v1/widget-sessions` | Create session token | Widget Sessions | | `DELETE` | `/api/v1/widget-sessions/{id}` | Revoke widget session | Widget Sessions | | `GET` | `/api/v1/widget-sessions/preview` | Create automatic dashboard preview tokens | Widget Sessions | | `POST` | `/api/v1/widget-sessions/verify-installation` | Verify widget installation | Widget Sessions | | `POST` | `/api/v1/widget/appearance/rerender` | Rerender stage base | Widget API | | `GET` | `/api/v1/widget/badges` | Get widget badge catalog | Widget API | | `GET` | `/api/v1/widget/beginners-luck/result` | Get the buddy's Beginner's Luck result | Beginner's Luck | | `GET` | `/api/v1/widget/boosters/active` | The buddy’s currently active boosters | Widget | | `GET` | `/api/v1/widget/boosters/catalog` | Buyable boosters for this tenant | Widget | | `POST` | `/api/v1/widget/boosters/purchase` | Buy a catalog booster — 400 insufficient_balance when too few coins | Widget | | `POST` | `/api/v1/widget/brag/share-profile` | Build the Brag Button "share my profile" payload | Widget | | `POST` | `/api/v1/widget/brag/slack-post` | Send a Win-State brag to the tenant Slack/Teams webhook | Widget | | `POST` | `/api/v1/widget/brag/telemetry` | Record one brag funnel event | Widget | | `POST` | `/api/v1/widget/brag/win-state` | Build the full Brag Button Win-State payload + enabled channels | Widget | | `GET` | `/api/v1/widget/buddy` | Get widget buddy | Widget API | | `POST` | `/api/v1/widget/buddy/equip-legacy-item` | Temp-equip a legacy crown for the returning-champion scene | Widget | | `POST` | `/api/v1/widget/buddy/hatched` | Record that the hatch ceremony completed for the widget buddy | Widget API | | `POST` | `/api/v1/widget/buddy/pause` | Put the current buddy on vacation until a date | Widget | | `GET` | `/api/v1/widget/buddy/prestige` | Whether the current buddy can prestige, and why not if it cannot | Widget | | `POST` | `/api/v1/widget/buddy/prestige` | Prestige the current buddy — reset to stage 0 for a prestige level | Widget | | `PATCH` | `/api/v1/widget/buddy/profile` | Update the buddy public Profile Page preferences | Widget API | | `POST` | `/api/v1/widget/buddy/resume` | End the current buddy vacation early | Widget | | `GET` | `/api/v1/widget/buddy/returning-champion` | Get the Returning Champion re-onboarding scene payload | Widget | | `POST` | `/api/v1/widget/buddy/returning-champion/dismiss` | Dismiss the Returning Champion scene | Widget | | `PATCH` | `/api/v1/widget/buddy/seo` | Set the per-buddy search-indexing preference | Widget API | | `POST` | `/api/v1/widget/buddy/share` | Mint (or fetch) the public share link for the widget buddy | Widget API | | `POST` | `/api/v1/widget/buddy/share/events` | Record a share-sheet funnel event (opened) | Widget API | | `GET` | `/api/v1/widget/buddy/vacation-status` | Current buddy vacation status | Widget | | `GET` | `/api/v1/widget/causes/counters` | List symbolic cause counters for the current buddy / team / tenant | Widget | | `GET` | `/api/v1/widget/causes/surfaces` | HTCH-70 — cause counters grouped per opted-in surface (banner / buddy strip / profile) | Widget | | `POST` | `/api/v1/widget/council/proposals` | Submit a narrative proposal (Council members only) | Widget | | `GET` | `/api/v1/widget/council/proposals/mine` | The buddy's own narrative proposals plus Council standing and quota | Widget | | `POST` | `/api/v1/widget/equip` | Equip or unequip items | Widget API | | `GET` | `/api/v1/widget/evolutions` | Get widget evolution timeline | Widget API | | `GET` | `/api/v1/widget/feed/team-events` | The buddy's team feed — cursor-paginated, newest first | Widget | | `POST` | `/api/v1/widget/feed/team-events/{id}/clap` | Toggle a 👏 clap on a feed item | Widget | | `GET` | `/api/v1/widget/foundations` | List the tenant's active foundation selections — read-only for widget rendering. | Foundations | | `GET` | `/api/v1/widget/founding-cohort/status` | Founding Cohort status for the current buddy | Widget | | `POST` | `/api/v1/widget/free-lunch/{id}/acknowledge` | Acknowledge a Free Lunch banner | Free Lunch | | `GET` | `/api/v1/widget/free-lunch/notification` | Get the buddy's pending Free Lunch banner | Free Lunch | | `POST` | `/api/v1/widget/group-quests/{id}/join` | Join a Group Quest — idempotent (already_joined on re-join) | Widget | | `POST` | `/api/v1/widget/group-quests/{id}/leave` | Leave a Group Quest — the buddy’s prior contribution stays counted | Widget | | `GET` | `/api/v1/widget/group-quests/active` | List the active Group Quests visible to the current buddy | Widget | | `DELETE` | `/api/v1/widget/hexad-survey/me` | Delete the buddy raw response (GDPR / consent withdrawal) | Hexad survey | | `GET` | `/api/v1/widget/hexad-survey/me` | Fetch the current buddy Hexad response | Hexad survey | | `GET` | `/api/v1/widget/hexad-survey/questions` | List Hexad survey question metadata | Hexad survey | | `POST` | `/api/v1/widget/hexad-survey/responses` | Submit (or replace) the buddy Hexad survey response | Hexad survey | | `POST` | `/api/v1/widget/items/{id}/gift` | Gift a marketplace item to a teammate | Widget | | `POST` | `/api/v1/widget/kudos` | Send a kudos to a teammate | Widget | | `GET` | `/api/v1/widget/kudos/given` | List the buddy’s most recent sent kudos + lifetime count | Widget | | `GET` | `/api/v1/widget/kudos/received` | List the buddy’s most recent received kudos | Widget | | `GET` | `/api/v1/widget/kudos/types` | List the effective kudo taxonomy for the composer | Widget | | `GET` | `/api/v1/widget/leaderboard` | Get leaderboard | Leaderboard | | `GET` | `/api/v1/widget/leagues/boss-fight` | The season's Boss Fight challenge — progress, target, leaderboard | Widget | | `GET` | `/api/v1/widget/leagues/me` | The buddy's live league standing — tier, cohort, countdown | Widget | | `POST` | `/api/v1/widget/leagues/off-season/scouting-quest` | Start the cohort pre-season scouting quest with a prediction | Widget | | `POST` | `/api/v1/widget/leagues/off-season/scouting-quest/join` | Join the cohort's already-started scouting quest | Widget | | `GET` | `/api/v1/widget/leagues/off-season/status` | The off-season window — Mystery Box boost, wardrobe drops, scouting quest | Widget | | `GET` | `/api/v1/widget/leagues/seasons/{seasonId}/highlights/me` | The buddy's personalized season-closing highlights | Widget | | `GET` | `/api/v1/widget/leagues/seasons/latest/highlights/me` | The buddy's latest closed season-closing highlights | Widget | | `GET` | `/api/v1/widget/lottery/active-entries` | The buddy's live lottery entries with their next-draw time | Widget | | `GET` | `/api/v1/widget/lottery/last-win` | The buddy's most recent lottery win, if any | Widget | | `GET` | `/api/v1/widget/marketplace` | Get widget marketplace | Widget API | | `GET` | `/api/v1/widget/marketplace/composition-status` | Poll an outfit composition variant | Widget API | | `POST` | `/api/v1/widget/marketplace/fomo` | Marketplace FOMO poll | Widget | | `POST` | `/api/v1/widget/marketplace/items/{id}/track-view` | Track marketplace item impression | Widget API | | `GET` | `/api/v1/widget/marketplace/outfits` | List saved outfits | Widget API | | `POST` | `/api/v1/widget/marketplace/outfits` | Save a new outfit | Widget API | | `DELETE` | `/api/v1/widget/marketplace/outfits/{id}` | Delete a saved outfit | Widget API | | `PATCH` | `/api/v1/widget/marketplace/outfits/{id}/activate` | Activate a saved outfit | Widget API | | `POST` | `/api/v1/widget/marketplace/preview-outfit` | Preview an outfit composition | Widget API | | `POST` | `/api/v1/widget/mentor/availability` | Toggle the current buddy’s mentor availability | Widget | | `POST` | `/api/v1/widget/mentor/sessions` | Self-report a mentoring session | Widget | | `GET` | `/api/v1/widget/mentor/sessions/me` | List the buddy’s recent mentor sessions + hour aggregates | Widget | | `GET` | `/api/v1/widget/mentor/team/{id}/mentors` | List a team’s available mentors with contact deep links | Widget | | `GET` | `/api/v1/widget/mission-anchor` | Get the Mission Anchor payload | Widget API | | `POST` | `/api/v1/widget/mystery-box/claim` | Open the Mystery Box — 409 with next_eligible_at when the daily cap is spent | Widget | | `GET` | `/api/v1/widget/mystery-box/state` | Mystery Box state — eligible / capped / locked | Widget | | `GET` | `/api/v1/widget/narrative` | Get the tenant narrative | Widget API | | `GET` | `/api/v1/widget/narrative/arc` | Get the Program Chapters arc | Widget API | | `GET` | `/api/v1/widget/next-best-action` | Get the single next-best-action recommendation | Widget API | | `GET` | `/api/v1/widget/notifications` | Paginated notification feed for the current buddy | Widget | | `POST` | `/api/v1/widget/notifications/{id}/dismiss` | Read + dismiss a single notification (HTCH-76) | Widget | | `POST` | `/api/v1/widget/notifications/{id}/read` | Mark a single notification read | Widget | | `POST` | `/api/v1/widget/notifications/{id}/snooze` | Snooze a notification for a number of hours (HTCH-76) | Widget | | `POST` | `/api/v1/widget/notifications/dismiss-all` | Read + dismiss every notification for the buddy | Widget | | `GET` | `/api/v1/widget/notifications/unread-count` | Unread, non-dismissed notification count (badge) | Widget | | `GET` | `/api/v1/widget/operations/{id}` | Get widget operation | Widget API | | `GET` | `/api/v1/widget/path` | Get the active path for the buddy’s audience | Widget API | | `GET` | `/api/v1/widget/path/{key}` | Get a specific path for a buddy | Widget API | | `POST` | `/api/v1/widget/path/{key}/sub-steps/{subKey}/complete` | Manually mark a sub-step complete | Widget API | | `GET` | `/api/v1/widget/profile/history` | Visual Grave history — faded lost streaks + reclaimable items | Widget | | `POST` | `/api/v1/widget/profile/history/items/{id}/reclaim` | Earn a relinquished starter-rare back — fires recovery.streak_restored | Widget | | `GET` | `/api/v1/widget/profile/sunk-cost-summary` | Sunk-Cost 'Your journey so far' summary for the current buddy | Widget | | `POST` | `/api/v1/widget/profile/sunk-cost-summary/acknowledge` | Acknowledge the Sunk-Cost panel on first open — fires the paired White Hat celebration.milestone_acknowledged (idempotent per buddy) | Widget | | `POST` | `/api/v1/widget/purchase` | Purchase item | Widget API | | `POST` | `/api/v1/widget/rendered` | Record a successful widget render | Widget API | | `GET` | `/api/v1/widget/social-norms/today` | The buddy's positive-framing team norms for today | Widget | | `GET` | `/api/v1/widget/state` | Get aggregate widget state | Widget API | | `GET` | `/api/v1/widget/streak/{key}` | Get widget streak | Widget API | | `POST` | `/api/v1/widget/teams/{id}/leave` | Leave a team — blocked for a sole lead until another is promoted | Widget | | `GET` | `/api/v1/widget/teams/me` | Get the current buddy’s team, role and members | Widget | | `GET` | `/api/v1/widget/theme` | Live widget theme | Widget API | | `GET` | `/api/v1/widget/tokens` | Get the buddy’s token wallet | Widget API | | `POST` | `/api/v1/widget/track` | Track event from browser | Widget API | | `GET` | `/healthz` | Liveness probe (top-level alias) | Health | | `GET` | `/readyz` | Readiness probe (top-level alias) | Health | | `GET` | `/version` | Build metadata (top-level alias) | Health | {/* ENDPOINTS_END */} --- # SDK (JavaScript / TypeScript) > Complete method reference for @hatched/sdk-js — HatchedClient, resources, error classes. Source: https://docs.hatched.live/docs/reference/sdk-js {/* AUTO-GENERATED from packages/sdk-js by apps/docs/scripts/generate-sdk-reference.ts. Edit JSDoc in the SDK source; do not edit this file directly. */} Package: [`@hatched/sdk-js`](https://npmjs.com/package/@hatched/sdk-js) ```bash pnpm add @hatched/sdk-js ``` ## HatchedClient Official Hatched SDK client for JavaScript/TypeScript. Server-side (secret key): ```ts const hatched = new HatchedClient({ apiKey: process.env.HATCHED_API_KEY!, }); const egg = await hatched.eggs.create({ userId: 'user_designer_priya' }); await hatched.eggs.updateStatus(egg.eggId, 'ready'); const op = await hatched.eggs.hatch(egg.eggId); ``` Browser (publishable key, scoped): ```ts const hatched = new HatchedClient({ publishableKey: 'hatch_pk_xxxxxxxx', }); const buddy = await hatched.buddies.get('bdy_abc'); ``` ### `HatchedClient.health()` ```ts health() ``` Health check; returns API status metadata. ### `HatchedClient.getRateLimitInfo()` ```ts getRateLimitInfo() ``` Latest `X-RateLimit-*` snapshot from the most recent response. ### `HatchedClient.getLastRequestId()` ```ts getLastRequestId(): string | null ``` Request id of the most recent response (for support correlation). ### `HatchedClient.getLastRetryMetadata()` ```ts getLastRetryMetadata() ``` Retry metadata from the most recent request. `attempts === 1` means the call succeeded on the first try; higher means at least one retry happened. Useful for tracing and observability: ```ts await hatched.events.send({ eventId, userId: 'user_42', type: 'lesson_completed' }); const retry = hatched.getLastRetryMetadata(); if (retry && retry.attempts > 1) { logger.info({ attempts: retry.attempts, reasons: retry.reasons }); } ``` ## EggsResource ### `EggsResource.create()` ```ts create(params: CreateEggParams, signal?: AbortSignal): Promise ``` Creates a new pending egg bound to an external user. New eggs start in `waiting`; call `updateStatus(eggId, 'ready')` before `hatch()`. Pass `ensure: true` during a first-run bootstrap to reuse the user's existing `waiting`/`ready` egg instead of creating one. @example ```ts // Greenwave Learning Co. — a new instructional designer joins the workspace. const egg = await hatched.eggs.create({ userId: 'user_designer_priya' }); await hatched.eggs.updateStatus(egg.eggId, 'ready'); // Multi-audience workspace: bind the buddy to the right audience up front // so audience-scoped widgets (streak/badges/marketplace) resolve. await hatched.eggs.create({ userId: 'user_rep_amir', audience: 'sales_rep', ensure: true }); ``` ### `EggsResource.get()` ```ts get(eggId: string, signal?: AbortSignal): Promise ``` Fetches the canonical state of a single egg. ### `EggsResource.list()` ```ts list(params: ListEggsParams = {}): Promise ``` Lists eggs with optional filters. ### `EggsResource.updateStatus()` ```ts updateStatus(eggId: string, status: 'ready' | 'cancelled', signal?: AbortSignal): Promise ``` Transitions an egg to `ready` or `cancelled`. The API only permits `ready` and `cancelled` terminal statuses via this endpoint. ### `EggsResource.hatch()` ```ts hatch(eggId: string, signal?: AbortSignal): Promise ``` Kicks off an async hatch operation. Poll the returned operationId via `operations.wait()` to resolve when the buddy art is ready. The egg must already be in `ready` status. ## BuddiesResource ### `BuddiesResource.get()` ```ts get(buddyId: string, signal?: AbortSignal): Promise ``` Fetches a buddy by id. ### `BuddiesResource.list()` ```ts list(params: BuddyListParams = {}): Promise ``` Lists buddies with optional filters. @example ```ts // Greenwave Learning Co. — list the first page of active buddies. const { data: buddies } = await hatched.buddies.list({ status: 'active', limit: 25, }); ``` ### `BuddiesResource.updateName()` ```ts updateName(buddyId: string, name: string, signal?: AbortSignal): Promise ``` ### `BuddiesResource.archive()` ```ts archive(buddyId: string, signal?: AbortSignal): Promise ``` ### `BuddiesResource.updateSkills()` ```ts updateSkills(buddyId: string, updates: SkillUpdate[], signal?: AbortSignal) ``` ### `BuddiesResource.earn()` ```ts earn(buddyId: string, params: EarnCoinsParams, idempotencyKey?: string, signal?: AbortSignal) ``` Adds coins to a buddy's ledger for a given reason. Alias: `BuddiesResource.earnCoins`. ### `BuddiesResource.spend()` ```ts spend(buddyId: string, params: SpendCoinsParams, idempotencyKey?: string, signal?: AbortSignal) ``` Debits coins from a buddy's ledger. Fails with `InsufficientBalanceError` if the buddy doesn't have enough. ### `BuddiesResource.awardBadge()` ```ts awardBadge(buddyId: string, badgeKey: string, reason?: string, signal?: AbortSignal) ``` ### `BuddiesResource.getBadges()` ```ts getBadges(buddyId: string, signal?: AbortSignal): Promise<{ badges: Badge[] }> ``` ### `BuddiesResource.equip()` ```ts equip(buddyId: string, params: EquipItemsParams, signal?: AbortSignal): Promise ``` Equips or unequips items on a buddy. ### `BuddiesResource.rerenderAppearance()` ```ts rerenderAppearance(buddyId: string, signal?: AbortSignal): Promise ``` Regenerate the buddy's bare stage base image. Use after a hard generation failure or when `appearance.status === 'failed'` with `code: 'needs_rerender'`. Equipped items are removed from the rendered set; re-equip after the appearance returns to `ready`. ### `BuddiesResource.purchaseItem()` ```ts purchaseItem(buddyId: string, itemId: string, idempotencyKey?: string, signal?: AbortSignal) ``` ### `BuddiesResource.getPurchasedItems()` ```ts getPurchasedItems(buddyId: string, signal?: AbortSignal) ``` ### `BuddiesResource.getEvolution()` ```ts getEvolution(buddyId: string, signal?: AbortSignal) ``` ### `BuddiesResource.evolve()` ```ts evolve(buddyId: string, signal?: AbortSignal) ``` Starts the async operation that advances a ready buddy to its next evolution stage. Use after `events.send()` returns `effects.evolutionReady === true` when auto-evolve is disabled. ### `BuddiesResource.getProgression()` ```ts getProgression(buddyId: string, signal?: AbortSignal) ``` ### `BuddiesResource.tokens()` ```ts tokens(buddyId: string, signal?: AbortSignal): Promise ``` Typed token balances for a buddy, grouped into primary (spendable) and progression (accumulate-only). Returns null for either slot if the customer has not configured that kind. ### `BuddiesResource.evolutions()` ```ts evolutions(buddyId: string, params: { page?: number; limit?: number; signal?: AbortSignal } = {}): Promise<{ data: BuddyEvolutionRecord[]; pagination: { page: number; limit: number; total: number }; }> ``` Paginated stage-transition timeline for a buddy (includes both prod and demo evolutions). ### `BuddiesResource.getUserSummary()` ```ts getUserSummary(userId: string, signal?: AbortSignal) ``` ### `BuddiesResource.prestigeStatus()` ```ts prestigeStatus(signal?: AbortSignal): Promise ``` F4.3 Prestige Loop — whether the widget buddy can prestige right now. A widget-token endpoint: `available: false` means the tenant has not enabled the prestige loop; `canPrestige: false` carries the blocking reason. Read this to decide whether to surface a Prestige CTA. ### `BuddiesResource.prestige()` ```ts prestige(signal?: AbortSignal): Promise ``` F4.3 Prestige Loop — prestige the widget buddy: reset it to evolution stage 0 in exchange for an incremented prestige level and a permanent prestige aura (Yu-kai Ch.9 #66 Crowning). A widget-token endpoint. Throws when the buddy fails any precondition (`prestige_not_available`). ## EventsResource ### `EventsResource.send()` ```ts send(params: SendEventParams, signal?: AbortSignal): Promise ``` Ingests a domain event. The same `eventId` returning twice yields the cached effect without re-applying rules. @example ```ts // Greenwave Learning Co. — Priya completed the week's module review. const effects = await hatched.events.send({ eventId: 'lesson_module_review_user_designer_priya_2026_05_03', userId: 'user_designer_priya', type: 'lesson_completed', properties: { score: 94, module: 'module_review_week_4' }, }); ``` ### `EventsResource.sendBatch()` ```ts sendBatch(events: SendEventParams[], signal?: AbortSignal): Promise<{ results: EventEffects[] }> ``` Sends a batch of events in a single call. ## OperationsResource ### `OperationsResource.get()` ```ts get(operationId: string, signal?: AbortSignal): Promise> ``` Fetches an operation's current status. ### `OperationsResource.wait()` ```ts wait(operationId: string, options: WaitOptions = {}): Promise> ``` Polls an operation until it reaches a terminal status (`completed`, `failed`, or `cancelled`). @throws `Error` if the operation doesn't finish before `timeoutMs` elapses. @example ```ts await hatched.eggs.updateStatus(egg.eggId, 'ready'); const op = await hatched.eggs.hatch(egg.eggId); const finished = await hatched.operations.wait(op.operationId); ``` ### `OperationsResource.waitForCompletion()` ```ts waitForCompletion(operationId: string, options: { timeout?: number; interval?: number; signal?: AbortSignal } = {}): Promise> ``` @deprecated Use `OperationsResource.wait` instead. ## WidgetSessionsResource ### `WidgetSessionsResource.create()` ```ts create(params: CreateSessionParams, signal?: AbortSignal): Promise ``` Mints a short-lived widget session token for browser/interactive widgets. Never ship a secret API key to the browser — always go through this endpoint. ### `WidgetSessionsResource.revoke()` ```ts revoke(sessionId: string, signal?: AbortSignal): Promise ``` ## EmbedTokensResource ### `EmbedTokensResource.create()` ```ts create(params: CreateEmbedTokenParams, signal?: AbortSignal): Promise ``` Mints a signed token for a read-only embedded widget. ## WebhooksResource ### `WebhooksResource.list()` ```ts list(signal?: AbortSignal): Promise ``` Lists webhook endpoints registered for the current customer. ### `WebhooksResource.create()` ```ts create(params: CreateWebhookParams, signal?: AbortSignal): Promise ``` Registers a new webhook endpoint. ### `WebhooksResource.delete()` ```ts delete(endpointId: string, signal?: AbortSignal): Promise ``` Deletes a webhook endpoint. ### `WebhooksResource.deliveries()` ```ts deliveries(params: ListDeliveriesParams): Promise> ``` Lists recent deliveries for a given endpoint. ### `WebhooksResource.replay()` ```ts replay(endpointId: string, deliveryId: string, signal?: AbortSignal): Promise ``` Replays a specific delivery attempt. ### `WebhooksResource.rotateSecret()` ```ts rotateSecret(endpointId: string, signal?: AbortSignal): Promise<{ secret: string }> ``` Rotates the signing secret on a webhook endpoint. Returns the new plaintext secret — store it before the response goes out of scope. Recommended rollout: deploy your handler with `verifySignature` set to accept BOTH the old and new secret simultaneously, *then* call `rotateSecret()`. After every host has the new secret in its environment, remove the old one from the verifier list. See the [rotation playbook](https://docs.hatched.live/guides/verify-webhooks#rotate-the-signing-secret). ### `WebhooksResource.redeliver()` ```ts redeliver(endpointId: string, deliveryId: string, signal?: AbortSignal): Promise ``` Re-enqueues a stored webhook delivery. ### `WebhooksResource.verifySignature()` ```ts static verifySignature(rawBody: string | Buffer, signatureHeader: string, secret: string | readonly string[], options: VerifySignatureOptions = {}): boolean ``` Verifies the `X-Hatched-Signature` header for a webhook payload. Hatched signs `${timestamp}.${rawBody}` with HMAC-SHA256 and sends: - `X-Hatched-Signature: sha256=` - `X-Hatched-Timestamp: ` ← pass via `options.timestamp` Pass the **raw request body bytes** (not the parsed JSON) — any reformatting will invalidate the signature. Prefer the framework adapters in `@hatched/sdk-js/webhooks`, which extract both headers and the raw body for you. `secret` accepts either a single string or an array — pass an array during a secret-rotation window so the verifier accepts payloads signed by either the previous or the new secret. @example ```ts const valid = WebhooksResource.verifySignature( rawBody, req.headers['x-hatched-signature'], process.env.HATCHED_WEBHOOK_SECRET!, { timestamp: req.headers['x-hatched-timestamp'] }, ); if (!valid) return new Response('invalid signature', { status: 400 }); ``` ## HatchedError Base class for every error raised by the Hatched SDK. Every subclass carries the HTTP status, stable error code, optional details payload, and the request id the API echoed back for support correlation. _No public methods._ ## AuthError Shared base for 401/403 responses. _No public methods._ ## UnauthorizedError _No public methods._ ## ForbiddenError _No public methods._ ## PublishableKeyScopeError Raised when a request uses a publishable key for an operation the publishable key is not scoped for (e.g. mutation endpoints). _No public methods._ ## WidgetTokenScopeError Raised when a widget-token client tries to call a non-widget-token mutation. Widget tokens are scoped to one buddy/session. _No public methods._ ## NotFoundError _No public methods._ ## ValidationError _No public methods._ ## RateLimitError _No public methods._ ## InsufficientBalanceError _No public methods._ ## TooManyItemsError Raised when an equip request asks the buddy to wear more items than the image compositing pipeline can reliably render. The current cap is four — the SDK surfaces `max` and `attempted` on `details` so callers can show a precise error to the end-user. _No public methods._ ## CategoryConflictError Raised when an equip request tries to put two items in the same category slot (e.g. two head items). Only the `accessory` category allows stacking. _No public methods._ ## ConflictError _No public methods._ ## ConfigVersionMismatchError Raised when a buddy is pinned to a config version that does not match the one the caller expected (e.g. after a migration race). _No public methods._ ## NoPublishedConfigError Raised by `POST /eggs` (and the bootstrap flow) when the customer has not published a config version yet. `details.publish_url` points at the dashboard publish page. _No public methods._ ## ActiveEggLimitError Raised when `POST /eggs` would exceed the per-user active-egg cap. `details.active` lists the existing eggs (id + status) so you can hatch or cancel one — or retry the create with `?ensure=true` to reuse one. _No public methods._ ## UpstreamImageError _No public methods._ ## CreditInsufficientError Raised when an AI / generative request cannot be authorised because the customer has no available credits across any pool. The `details` object includes the amount required and what remains in each pool, plus a URL the caller can redirect to so the end-customer can top up. _No public methods._ ## EventQuotaExceededError Raised when a POST /events call would push the customer over the monthly event quota allowed by their plan. `reset_at` is an ISO timestamp for the first of the next UTC month; callers should back off until then or upgrade. _No public methods._ ## PlanFeatureLockedError Raised when the customer's plan does not include the requested feature (e.g. a Free tier customer trying to use the marketplace API). The SDK surfaces which plan is required so callers can prompt an upgrade. _No public methods._ ## PlayersResource Player Zero — the reserved per-workspace demo player (`user_id "player-0"`). Every dashboard widget preview binds to this buddy, and it is the recommended first test user during integration: send events as `player-0` and watch them land without polluting real user data. ### `PlayersResource.zero()` ```ts zero(signal?: AbortSignal): Promise ``` Create-or-get the workspace demo player. Idempotent: a second call returns the existing buddy with `created: false`. Instant — returns a safe placeholder image first, then Hatched queues a background base render so Player Zero settles into the workspace's visual style. @example ```ts const { buddy } = await hatched.players.zero(); await hatched.events.ingest({ userId: buddy.userId, // "player-0" type: 'lesson.completed', eventId: 'evt_demo_1', }); ``` ### `PlayersResource.zeroStatus()` ```ts zeroStatus(signal?: AbortSignal): Promise ``` Read Player Zero's status without provisioning it. Never creates the player — safe for polling (e.g. an activation checklist). ## GatesResource Generic spend-to-unlock primitive. Customers define gates in their dashboard (gate_key, token_key, cost, metadata). A buddy calls `unlock` to spend the configured token cost and flip the gate open — idempotent: repeat calls return `alreadyUnlocked: true` without touching the economy. ### `GatesResource.list()` ```ts list(signal?: AbortSignal): Promise<{ gates: TokenGate[] }> ``` Lists gates configured on this customer. Secret-key only. ### `GatesResource.unlock()` ```ts unlock(buddyId: string, gateKey: string, signal?: AbortSignal): Promise ``` Buddy spends `gate.cost` of `gate.tokenKey` to unlock `gateKey`. Fails with `InsufficientBalanceError` if the buddy lacks tokens and with `ValidationError('progression_not_spendable')` if the gate references a progression token. ### `GatesResource.unlocks()` ```ts unlocks(buddyId: string, signal?: AbortSignal): Promise<{ unlocks: BuddyUnlock[] }> ``` List gates a buddy has unlocked. ## PathsResource Guided journey primitive — a path is an ordered list of steps; each step holds an ordered list of sub-steps with an optional completion condition. Sub-step completions advance the buddy through the path automatically (rule-engine) or manually via `completeSubStep`. The `HttpClient` auto-converts wire snake_case → camelCase on every response, so resource methods read camelCase fields directly without an intermediate DTO mapping layer. ### `PathsResource.list()` ```ts list(audience?: string, signal?: AbortSignal): Promise ``` ### `PathsResource.get()` ```ts get(definitionId: string, signal?: AbortSignal): Promise ``` ### `PathsResource.create()` ```ts create(params: CreatePathDefinitionParams, signal?: AbortSignal): Promise ``` ### `PathsResource.update()` ```ts update(definitionId: string, params: UpdatePathDefinitionParams, signal?: AbortSignal): Promise ``` ### `PathsResource.delete()` ```ts delete(definitionId: string, signal?: AbortSignal): Promise ``` ### `PathsResource.setActive()` ```ts setActive(definitionId: string, isActive: boolean, signal?: AbortSignal): Promise ``` Atomic single-active activation: deactivates every other path on the same (customer, audience) in a single transaction. ### `PathsResource.addStep()` ```ts addStep(definitionId: string, params: CreatePathStepParams, signal?: AbortSignal): Promise ``` ### `PathsResource.updateStep()` ```ts updateStep(definitionId: string, stepId: string, params: UpdatePathStepParams, signal?: AbortSignal): Promise ``` ### `PathsResource.deleteStep()` ```ts deleteStep(definitionId: string, stepId: string, signal?: AbortSignal): Promise ``` ### `PathsResource.reorderSteps()` ```ts reorderSteps(definitionId: string, ordering: Array<{ id: string; ordinal: number }>, signal?: AbortSignal): Promise ``` ### `PathsResource.addSubStep()` ```ts addSubStep(definitionId: string, stepId: string, params: CreatePathSubStepParams, signal?: AbortSignal): Promise ``` ### `PathsResource.updateSubStep()` ```ts updateSubStep(definitionId: string, stepId: string, subStepId: string, params: UpdatePathSubStepParams, signal?: AbortSignal): Promise ``` ### `PathsResource.deleteSubStep()` ```ts deleteSubStep(definitionId: string, stepId: string, subStepId: string, signal?: AbortSignal): Promise ``` ### `PathsResource.reorderSubSteps()` ```ts reorderSubSteps(definitionId: string, stepId: string, ordering: Array<{ id: string; ordinal: number }>, signal?: AbortSignal): Promise ``` ### `PathsResource.getForBuddy()` ```ts getForBuddy(buddyId: string, pathKey: string, signal?: AbortSignal): Promise ``` ### `PathsResource.completeSubStep()` ```ts completeSubStep(buddyId: string, pathKey: string, subStepKey: string, signal?: AbortSignal): Promise ``` Manually mark a sub-step complete. Idempotent on (buddy, sub-step). Returns cascade flags so callers can paint celebrations without an extra round-trip. ## MarketplaceResource ### `MarketplaceResource.list()` ```ts list(params: MarketplaceListParams = {}): Promise ``` Lists widget marketplace items visible to the current buddy/session. ### `MarketplaceResource.gift()` ```ts gift(params: GiftItemParams): Promise ``` F2.4 Social Treasure — gift a marketplace item to a teammate. The current buddy pays; the item lands in the recipient's inventory with gift metadata. Works for gift-only and ordinary items alike. Throws a 402-class error when the sender is short on coins; returns `duplicate: true` when the same gift is re-sent inside the 60s accident-click window. ### `MarketplaceResource.previewOutfit()` ```ts previewOutfit(slotItemMap: SlotItemMap, signal?: AbortSignal): Promise ``` Previews a slot-to-item outfit without mutating the buddy. Returns a ready cached variant or a pending variant id for polling. ### `MarketplaceResource.compositionStatus()` ```ts compositionStatus(variantId: string, signal?: AbortSignal): Promise ``` Polls an outfit composition variant until it is ready or failed. ### `MarketplaceResource.listOutfits()` ```ts listOutfits(signal?: AbortSignal): Promise ``` Lists saved outfits for the current buddy/session. ### `MarketplaceResource.saveOutfit()` ```ts saveOutfit(params: SaveOutfitParams, idempotencyKey?: string, signal?: AbortSignal): Promise ``` Saves a named outfit for the current buddy/session. ### `MarketplaceResource.activateOutfit()` ```ts activateOutfit(outfitId: string, signal?: AbortSignal): Promise ``` Activates a saved outfit and delegates to the equip pipeline. ### `MarketplaceResource.deleteOutfit()` ```ts deleteOutfit(outfitId: string, signal?: AbortSignal): Promise ``` Deletes a saved outfit for the current buddy/session. ## BadgesResource ### `BadgesResource.list()` ```ts list(params: BadgeListParams = {}): Promise ``` Lists the current widget buddy's earned and locked badge catalog. ## LeaderboardResource ### `LeaderboardResource.get()` ```ts get(params: LeaderboardParams = {}): Promise ``` Returns the current widget buddy's leaderboard in top, around-me, or hybrid mode. Pass `scope: 'team'` to restrict to the buddy's active team roster. ## NextBestActionResource ### `NextBestActionResource.get()` ```ts get(signal?: AbortSignal): Promise ``` HTCH-26 — Returns the single highest-priority next-best-action for the widget session's buddy. Cached server-side for 30s per buddy. ## TeamsResource F2.1 Teams — widget-token resource. Lets an embedded buddy see its team and leave it. Admin team CRUD lives in the dashboard, not the SDK. ### `TeamsResource.me()` ```ts me(signal?: AbortSignal): Promise ``` The current widget buddy's team, role and members (PII-filtered). ### `TeamsResource.leave()` ```ts leave(teamId: string, signal?: AbortSignal): Promise<{ left: boolean }> ``` Leave a team. A sole lead is rejected with a `single_lead_cannot_leave` conflict until another lead is promoted. ## KudosResource F2.3 Kudos — widget-token resource. Lets an embedded buddy send peer recognition and read its recent received / given kudos. A send 429s when the workspace daily cap is reached; the thrown error carries `retry_after_seconds`. ### `KudosResource.send()` ```ts send(params: SendKudosParams): Promise ``` Send a kudos to a teammate. @example ```ts // Greenwave Learning Co. — a senior designer recognises a junior's question. await hatched.kudos.send({ toBuddyId: 'buddy_priya_buddy', kudoTypeKey: 'patient_teacher', message: 'Your follow-up on the module review walkthrough was exactly the prompt I needed.', }); ``` ### `KudosResource.types()` ```ts types(signal?: AbortSignal): Promise ``` The effective kudo taxonomy for the composer's type picker. ### `KudosResource.received()` ```ts received(limit = 10, signal?: AbortSignal): Promise ``` The buddy's most recent received kudos (Trophy Shelf). ### `KudosResource.given()` ```ts given(limit = 10, signal?: AbortSignal): Promise ``` The buddy's most recent sent kudos plus the lifetime assist count. ## GroupQuestsResource F2.5 Group Quest — widget-token resource. Lets an embedded buddy list the active quests visible to it, opt in (join) and reconsider (leave). Gated by the `group_quest` plan feature — calls 403 with `plan_feature_locked` on a workspace whose plan does not entitle Group Quest. ### `GroupQuestsResource.list()` ```ts list(signal?: AbortSignal): Promise ``` The active quests visible to the current buddy. ### `GroupQuestsResource.join()` ```ts join(questId: string, signal?: AbortSignal): Promise ``` Join a quest. Idempotent — a second join returns `already_joined: true` with no further write. ### `GroupQuestsResource.leave()` ```ts leave(questId: string, signal?: AbortSignal): Promise ``` Leave a quest. The buddy's prior contribution stays counted toward the team's progress — only the roster membership is removed. ## AdminGroupQuestsResource F2.5 Group Quest — tenant admin resource (secret key / dashboard JWT). Backs the Planner "Group Quest" drawer: CRUD, the publish transition, and the manual `forceResolve` watchdog override. ### `AdminGroupQuestsResource.list()` ```ts list(params: { status?: GroupQuestStatus; teamId?: string } = {}, signal?: AbortSignal): Promise ``` List the tenant's quests, optionally filtered by status / team. ### `AdminGroupQuestsResource.create()` ```ts create(params: CreateGroupQuestParams, signal?: AbortSignal): Promise ``` Create a quest (always starts as a draft). ### `AdminGroupQuestsResource.update()` ```ts update(questId: string, params: UpdateGroupQuestParams, signal?: AbortSignal): Promise ``` Patch a quest — draft fields, active deadline-extension, or cancel. ### `AdminGroupQuestsResource.publish()` ```ts publish(questId: string, signal?: AbortSignal): Promise ``` Publish a draft quest (draft → active). ### `AdminGroupQuestsResource.delete()` ```ts delete(questId: string, signal?: AbortSignal): Promise ``` Delete a quest — only draft or cancelled quests may be removed. ### `AdminGroupQuestsResource.forceResolve()` ```ts forceResolve(questId: string, signal?: AbortSignal): Promise ``` HTCH-56 — manually resolve an active quest now, overriding the cron watchdog. Distributes rewards on a hit, closes blame-free on a miss. ## MentorResource F2.6 Mentorship (visibility-only) — widget-token resource. The buddy toggles its own mentor availability, reads a team's available mentors, and self-reports mentoring hours. Hatched counts status and renders a contact deep link; it never matches, pairs, or messages. ### `MentorResource.setAvailability()` ```ts setAvailability(params: { available: boolean; signal?: AbortSignal }): Promise<{ available: boolean }> ``` Toggle the current buddy's mentor availability flag. ### `MentorResource.teamMentors()` ```ts teamMentors(teamId: string, signal?: AbortSignal): Promise ``` List a team's available mentors with contact deep links. ### `MentorResource.logSession()` ```ts logSession(params: LogSessionParams): Promise ``` Self-report a mentoring session (honor system). ### `MentorResource.sessionsForMe()` ```ts sessionsForMe(signal?: AbortSignal): Promise ``` The buddy's recent mentor sessions plus all-time / season hour totals. ## BragResource HTCH-60 — F2.7 Brag Button — widget-token resource. Records the share funnel (consent modal opened → channel clicked → post sent → dismissed) and dispatches a Slack/Teams webhook post. Every call here corresponds to an explicit user action in the consent modal — there is no auto-share path (Codex ethics rule). ### `BragResource.recordTelemetry()` ```ts recordTelemetry(params: RecordBragTelemetryParams): Promise ``` Record one brag funnel event for the HTCH-61 Planner telemetry tab. Best-effort — the server never fails the share flow on a telemetry write. ### `BragResource.sendSlackPost()` ```ts sendSlackPost(params: SendBragSlackPostParams): Promise ``` Send a Win-State brag to the tenant's configured Slack/Teams incoming webhook. Only call this after the user pressed "Send" in the consent modal. Throws `webhook_failed` when delivery times out or is rejected. ## TeamEventsResource HTCH-63 — F2.11 SeeSaw Bump feed — `teamEvents` sub-resource. Lists the buddy's team feed and toggles the idempotent 👏 clap. Clapping is one-per-buddy; a repeat call unclaps. Self-clap is rejected with a 400 `self_clap_forbidden`. ### `TeamEventsResource.list()` ```ts list(params: ListTeamEventsParams = {}): Promise ``` The buddy's team feed — cursor-paginated, newest first. ### `TeamEventsResource.clap()` ```ts clap(eventId: string, signal?: AbortSignal): Promise ``` Toggle a 👏 clap on a feed item. Idempotent — a repeat call unclaps. ## FeedResource HTCH-63 — F2.11 SeeSaw Bump feed — top-level `feed` resource. Namespaces the `teamEvents` sub-resource so the public surface reads `client.feed.teamEvents.list(...)` / `client.feed.teamEvents.clap(id)`. _No public methods._ ## SocialNormsResource HTCH-62 — F2.9 Social Norm — widget-token resource. Reads the buddy's positive-framing team norms for today. Yu-kai Ch.9, the Petrified Forest study: negative descriptive norms are structurally forbidden, and norms below the believability floor are silently skipped server-side. A buddy with no active team gets an empty `norms` array. ### `SocialNormsResource.today()` ```ts today(signal?: AbortSignal): Promise ``` The current widget buddy's positive-framing team norms for today. ## CausesResource F2.12 Symbolic Cause Counter — tenant admin resource (secret key / dashboard JWT). Backs the Planner "Humanity Hero — Cause Counter" drawer: CRUD plus the 30-day believability simulation. ### `CausesResource.list()` ```ts list(signal?: AbortSignal): Promise ``` List the tenant's cause definitions. ### `CausesResource.create()` ```ts create(params: CreateCauseParams, signal?: AbortSignal): Promise ``` Create a cause definition (disabled by default). ### `CausesResource.update()` ```ts update(causeId: string, params: UpdateCauseParams, signal?: AbortSignal): Promise ``` Patch a cause definition. ### `CausesResource.delete()` ```ts delete(causeId: string, signal?: AbortSignal): Promise ``` Delete a cause definition. ### `CausesResource.preview30Days()` ```ts preview30Days(causeId: string, signal?: AbortSignal): Promise ``` Project how many symbolic units the current config would have produced from the last 30 days of eligible events — the believability simulation shown in the Planner drawer. ## FoundingCohortResource F2.13 Founding Cohort — tenant admin resource (secret key / dashboard JWT). Backs the Planner "Founding Cohort" drawer: the eligibility preview, the one-shot retroactive backfill, and the assignment history. The cohort config itself is read and written through the feature-config surface. ### `FoundingCohortResource.preview()` ```ts preview(signal?: AbortSignal): Promise ``` Project how many buddies the current config would mark. ### `FoundingCohortResource.backfill()` ```ts backfill(signal?: AbortSignal): Promise ``` Retroactively mark every currently-eligible buddy (idempotent). ### `FoundingCohortResource.listAudit()` ```ts listAudit(page = 1, signal?: AbortSignal): Promise<{ entries: FoundingCohortAuditEntry[]; page: number; has_more: boolean; }> ``` Paginated Founding Cohort assignment history. ## FlashSalesResource F3.9 Marketplace FOMO — tenant admin resource (secret key / dashboard JWT). Backs the Planner "Marketplace FOMO" drawer: list, schedule and cancel flash sales. The API runs a once-a-minute cron that starts scheduled sales and ends running ones; at most one sale runs per tenant at a time. ### `FlashSalesResource.list()` ```ts list(signal?: AbortSignal): Promise ``` List the tenant's flash sales — scheduled, running and recently ended. ### `FlashSalesResource.schedule()` ```ts schedule(params: ScheduleFlashSaleParams): Promise ``` Schedule a flash sale. Rejects a past start time or an overlapping window. ### `FlashSalesResource.cancel()` ```ts cancel(saleId: string, signal?: AbortSignal): Promise ``` Cancel a scheduled or running sale. A running sale's temporary discounts are cleared and `flash_sale.ended` fires, exactly as a natural end would. ## LotteryResource F3.11 Lottery (Rolling Reward) — tenant admin resource (secret key / dashboard JWT). Backs the Planner "Lottery" drawer: list and CRUD, the past-draw history, the live "Next draw" preview and a non-persisted draw simulation. The API runs a once-a-minute cron that resolves due draws. ### `LotteryResource.list()` ```ts list(signal?: AbortSignal): Promise ``` List the tenant's lottery definitions (active and paused). ### `LotteryResource.create()` ```ts create(params: CreateLotteryParams): Promise ``` Create a lottery definition. ### `LotteryResource.update()` ```ts update(lotteryId: string, params: UpdateLotteryParams): Promise ``` Update a lottery definition. ### `LotteryResource.delete()` ```ts delete(lotteryId: string, signal?: AbortSignal): Promise ``` Soft-delete a lottery — past draws stay queryable. ### `LotteryResource.draws()` ```ts draws(lotteryId: string, signal?: AbortSignal): Promise ``` Past draw history for a lottery, newest first. ### `LotteryResource.previewNextDraw()` ```ts previewNextDraw(lotteryId: string, signal?: AbortSignal): Promise ``` Current-period entry count and the next scheduled draw time. ### `LotteryResource.simulateDraw()` ```ts simulateDraw(lotteryId: string, signal?: AbortSignal): Promise ``` Simulate a draw with the current entries — grants nothing. ## EventBadgesResource F3.13 Event-Triggered Badge (Yu-kai Ch.11 #30 Easter Egg) — tenant admin resource (secret key / dashboard JWT). Backs the Planner "Surprise badge campaign" drawer: list with grant counts, create, update and delete. A campaign binds a badge to a time window; any buddy active inside the window earns the badge once. ### `EventBadgesResource.list()` ```ts list(signal?: AbortSignal): Promise ``` List the tenant's campaigns, newest window first, with grant counts. ### `EventBadgesResource.create()` ```ts create(params: CreateEventBadgeParams): Promise ``` Create an event-triggered badge campaign. ### `EventBadgesResource.update()` ```ts update(campaignId: string, params: UpdateEventBadgeParams): Promise ``` Update an event-triggered badge campaign. ### `EventBadgesResource.delete()` ```ts delete(campaignId: string, signal?: AbortSignal): Promise ``` Delete an event-triggered badge campaign. ## ProfileTemplatesResource F3.14 Profile Page Editor v2 (Yu-kai Ch.7 #11 Meaningful Choices) — tenant admin resource (secret key / dashboard JWT). Backs the Planner "Profile Page v2" drawer: the template gallery + CRUD, and the bulk-apply wizard that assigns a template to many buddies in one call. Plan-gated on `profile_pages_v2` (GROWTH+). ### `ProfileTemplatesResource.list()` ```ts list(signal?: AbortSignal): Promise ``` List the gallery — built-in system templates plus the tenant's own. ### `ProfileTemplatesResource.create()` ```ts create(params: CreateProfileTemplateParams): Promise ``` Create a custom profile-page template. ### `ProfileTemplatesResource.update()` ```ts update(templateId: string, params: UpdateProfileTemplateParams): Promise ``` Update a custom profile-page template. ### `ProfileTemplatesResource.delete()` ```ts delete(templateId: string, signal?: AbortSignal): Promise ``` Delete a custom template — its buddies revert to the tenant default. ### `ProfileTemplatesResource.applyBulk()` ```ts applyBulk(params: BulkApplyProfileTemplateParams): Promise<{ applied: number }> ``` Assign a template to many buddies in one statement. ## LeaguesResource F4.1 LEAGUES — end-user widget resource. Reads the widget buddy's live league standing, the season-long Boss Fight challenge and, after a season closes, the personalized season-closing highlights. Backs the `league` widget and the season-closing ceremony. ### `LeaguesResource.me()` ```ts me(signal?: AbortSignal): Promise ``` The widget buddy's live league standing for the active season. ### `LeaguesResource.bossFightProgress()` ```ts bossFightProgress(signal?: AbortSignal): Promise ``` The season-long Boss Fight challenge for the widget buddy — F4.2. The buddy's progress toward the season target, the deadline and the challenge leaderboard. An unavailable view means there is no active boss fight. ### `LeaguesResource.seasonHighlights()` ```ts seasonHighlights(seasonId: string, signal?: AbortSignal): Promise ``` The buddy's personalized season-closing highlights for a finalized season — best week, kudos sent, items collected and cohort role. ### `LeaguesResource.latestSeasonHighlights()` ```ts latestSeasonHighlights(signal?: AbortSignal): Promise ``` The buddy's latest finalized season-closing highlights. Useful for widgets that should show the most recent ceremony without first resolving a concrete season id. ## HexadSurveyResource HTCH-142 — Marczewski Hexad survey, widget-token scoped. The buddy fetches the question catalog, submits Likert answers, reads their own stored response, or withdraws consent (DELETE /me). The API is intentionally minimal — admin-only operations (audience recompute, aggregate views) are exposed through the dashboard, not the SDK. A widget-token client cannot reach them. ### `HexadSurveyResource.questions()` ```ts questions(signal?: AbortSignal): Promise ``` Question catalog + current consent version. ### `HexadSurveyResource.submit()` ```ts submit(params: SubmitHexadSurveyParams): Promise ``` UPSERT the buddy's response — re-takes overwrite in place. ### `HexadSurveyResource.me()` ```ts me(signal?: AbortSignal): Promise ``` The buddy's latest survey row, or `{ response: null }` when none. ### `HexadSurveyResource.deleteMine()` ```ts deleteMine(signal?: AbortSignal): Promise ``` Withdraw consent: deletes the raw answers + derived scores. The next nightly aggregation absorbs the lower response count; audience-level aggregates are preserved. ## AuthResource `client.auth.whoami()` lets a tenant verify a key during onboarding, CI, or `--health` style scripts without performing a side-effectful call. It returns the identity of the calling credential, the plan, and the full capability list — no separate dashboard round-trip needed. ### `AuthResource.whoami()` ```ts whoami(signal?: AbortSignal): Promise ``` ## NotificationsResource HTCH-75 — F3.1 Notification primitive — widget-token resource. The HTCH-76 banner widget polls `list` (which folds in the unread badge count and the vacation `paused_until`) plus `unreadCount`, and drives the buddy's reads, dismissals and snoozes. The primitive is universally entitled — every Faz 3+ feature that emits a message writes into this one feed. ### `NotificationsResource.list()` ```ts list(params: ListNotificationsParams = {}): Promise ``` The buddy's notification feed — cursor-paginated, newest first. ### `NotificationsResource.unreadCount()` ```ts unreadCount(signal?: AbortSignal): Promise ``` The unread, non-dismissed notification count for the badge. ### `NotificationsResource.dismissAll()` ```ts dismissAll(signal?: AbortSignal): Promise ``` Read + dismiss every notification for the buddy in one call. ### `NotificationsResource.markRead()` ```ts markRead(id: string, signal?: AbortSignal): Promise ``` Mark a single notification read. Returns the updated notification. ### `NotificationsResource.dismiss()` ```ts dismiss(id: string, signal?: AbortSignal): Promise ``` Read + dismiss a single notification (HTCH-76). ### `NotificationsResource.snooze()` ```ts snooze(id: string, hours?: number, signal?: AbortSignal): Promise ``` Snooze a notification for a number of hours, clamped server-side to the 1–24h band. `hours` defaults to 1 when omitted. Returns the updated notification. ## MysteryBoxResource HTCH-83 — F3.6 Mystery Box (Yu-kai Ch.11 Skinner Box) — widget-token resource. A once-a-day box: read `getState` to render it (eligible / capped / locked), then `claim` on tap. The daily cap of 1 is a hard-coded addiction guardrail that resets at UTC midnight; a `claim` past the cap throws a 409 whose body carries `next_eligible_at`. The draw is deterministically seeded per (buddy, UTC day), so it cannot be re-rolled. ### `MysteryBoxResource.getState()` ```ts getState(signal?: AbortSignal): Promise ``` The Mystery Box state for the current buddy. ### `MysteryBoxResource.claim()` ```ts claim(signal?: AbortSignal): Promise ``` Open the Mystery Box. Throws a 409 (`mystery_box_daily_cap`) when the daily cap is already spent. ## CouncilResource HTCH-108 — F4.6 Council Elitism — widget-token resource. A Council member proposes user-facing narrative copy and tracks their own submissions (Yu-kai Ch.7 #2 Elitism + #1 Narrative co-creation). Membership is the gate: `submitProposal` rejects non-members with `not_council_member`, and a non-enterprise tenant never has members. Over the weekly quota a submit throws a 429 (`rate_limited`). ### `CouncilResource.listMyProposals()` ```ts listMyProposals(signal?: AbortSignal): Promise ``` The buddy's own narrative proposals plus their Council standing and remaining weekly quota. ### `CouncilResource.submitProposal()` ```ts submitProposal(params: SubmitProposalParams): Promise ``` Submit a narrative proposal. Council members only. ## FreeLunchResource HTCH-44 — F1.10 Free Lunch (Yu-kai #24) — widget-token resource. The buddy widget polls `getNotification` on mount to surface the unexpected welcome credit banner; dismissing it posts `acknowledge`, which closes the granted → seen → dismissed funnel so the banner does not reappear. ### `FreeLunchResource.getNotification()` ```ts getNotification(signal?: AbortSignal): Promise ``` The buddy's most recent unacknowledged Free Lunch grant, or `has_pending: false` when there is nothing to show. ### `FreeLunchResource.acknowledge()` ```ts acknowledge(id: string, signal?: AbortSignal): Promise ``` Acknowledge a Free Lunch banner so it does not reappear. ## BeginnersLuckResource HTCH-43 — Beginner's Luck reveal — widget-token resource. The hatch ceremony calls `getResult` once when the act-5 celebration mounts. The evaluation is idempotent (HTCH-41), so a refresh mid-ceremony resolves to the same outcome. ### `BeginnersLuckResource.getResult()` ```ts getResult(signal?: AbortSignal): Promise ``` Idempotently evaluate Beginner's Luck for the buddy's first hatch and return the winner-only reveal payload. ## WidgetActivationResource Loader-level activation beacons for widget-token clients. The CDN loader calls this after a widget mounts so activation emails, referrals and product analytics do not count a copied snippet until it really rendered. ### `WidgetActivationResource.recordRendered()` ```ts recordRendered(params: RecordWidgetRenderedParams = {}): Promise ``` Record that one or more widgets rendered successfully. ## Configuration These shapes describe the object passed to `new HatchedClient({ ... })`. The three auth-mode variants (`ApiKeyConfig`, `PublishableKeyConfig`, `WidgetTokenConfig`) are mutually exclusive — pick one and the other key fields are forbidden. ### `ApiKeyConfig` ```ts export interface ApiKeyConfig extends BaseConfig { /** Secret API key (hatch_live_*, hatch_test_*). Server-only. */ apiKey: string; publishableKey?: never; widgetToken?: never; } ``` ### `PublishableKeyConfig` ```ts export interface PublishableKeyConfig extends BaseConfig { /** Browser-safe publishable key (hatch_pk_*). Scoped auth. */ publishableKey: string; apiKey?: never; widgetToken?: never; } ``` ### `WidgetTokenConfig` ```ts export interface WidgetTokenConfig extends BaseConfig { /** Widget embed/session token minted by the server. Browser-safe, buddy-scoped. */ widgetToken: string; apiKey?: never; publishableKey?: never; } ``` ### `HatchedClientConfig` ```ts export type HatchedClientConfig = | ApiKeyConfig | PublishableKeyConfig | WidgetTokenConfig; ``` ## Functions Top-level helpers exported from `@hatched/sdk-js`. These are framework-agnostic utilities you can call without a `HatchedClient` instance. ### `paginate()` ```ts paginate(fetchPage: (page: number) => Promise>, options: PaginateOptions = {}): AsyncIterableIterator ``` Walks an offset-paginated list endpoint, yielding each row as it arrives. `fetchPage` receives a 1-indexed page number and must return the offset envelope `{ data, meta }`. For cursor-paginated endpoints, use `paginateCursor` instead. @example ```ts for await (const buddy of paginate( (page) => hatched.buddies.list({ page, limit: 100, status: 'active' }), )) { if (buddy.id === target) break; // early-exit; iterator stops paging } ``` ### `paginateCursor()` ```ts paginateCursor(fetchPage: (cursor?: string) => Promise>, options: PaginateOptions = {}): AsyncIterableIterator ``` Walks a cursor-paginated list endpoint. `fetchPage` is invoked with `undefined` on the first call and the server-returned `nextCursor` on each subsequent call. Iteration stops automatically when `pagination.nextCursor === null`. Cursor pagination is Hatched's canonical shape for new list endpoints — it's stable under concurrent writes and supports unbounded streams. For legacy offset endpoints, use `paginate`. @example ```ts for await (const op of paginateCursor( (cursor) => hatched.operations.list({ cursor, limit: 100 }), )) { console.log(op.id); } ``` ### `collect()` ```ts collect(fetchPage: (page: number) => Promise>, options: PaginateOptions = {}): Promise ``` Drains an offset-paginated list endpoint into a single array. Convenient for one-off scripts; prefer `paginate` in long-running code so you don't hold the entire result set in memory. For cursor-paginated endpoints, use `collectCursor`. @example ```ts const allActive = await collect( (page) => hatched.buddies.list({ page, limit: 100, status: 'active' }), ); ``` ### `collectCursor()` ```ts collectCursor(fetchPage: (cursor?: string) => Promise>, options: PaginateOptions = {}): Promise ``` Drains a cursor-paginated list endpoint into a single array. Convenient for one-off scripts; prefer `paginateCursor` in long-running code so you don't hold the entire result set in memory. @example ```ts const allOps = await collectCursor( (cursor) => hatched.operations.list({ cursor, limit: 100 }), ); ``` ### `consoleLogger()` ```ts consoleLogger(): SdkLogger ``` Default logger — writes to `console.warn` for `warn`/`error` and to `console.log` for `debug`/`info`. Used when the host hasn't provided a logger but `debug` is on, or when the SDK needs to surface a warn-level message and the host left `logger` undefined. ### `verifyExpressRequest()` ```ts verifyExpressRequest(req: MinimalExpressRequest, secret: string | readonly string[], options: VerifyAdapterOptions = {}): VerifyResult ``` Verify the signature on an incoming Express request and return the parsed raw payload plus Hatched metadata headers. Pass `req` directly — the adapter reads `req.headers`, `req.body` (Buffer from `express.raw()`), or `req.rawBody` fallback. ### `verifyFastifyRequest()` ```ts verifyFastifyRequest(req: MinimalFastifyRequest, secret: string | readonly string[], options: VerifyAdapterOptions = {}): VerifyResult ``` ### `verifyHonoRequest()` ```ts verifyHonoRequest(c: MinimalHonoContext, secret: string | readonly string[], options: VerifyAdapterOptions = {}): Promise ``` ### `verifyNextAppRequest()` ```ts verifyNextAppRequest(req: MinimalFetchRequest, secret: string | readonly string[], options: VerifyAdapterOptions = {}): Promise ``` App Router (Route Handlers): pass the `Request` you received in `POST(req)`. ### `verifyNextPagesRequest()` ```ts verifyNextPagesRequest(req: MinimalPagesRequest, rawBody: Buffer | string, secret: string | readonly string[], options: VerifyAdapterOptions = {}): VerifyResult ``` Pages Router (API Routes): pass `(req)` plus the raw body you captured yourself. Because Next disables raw bytes by default, read them with something like `getRawBody(req)` from the `raw-body` npm package. ## Constants Frozen exported values. Use them in `switch` statements and comparisons instead of re-typing string literals — the type system catches typos and renames. ### `ErrorCode` Canonical Hatched API error code strings, surfaced as a typed object so consumers can `switch` on `err.code` without re-typing literals. Each value matches the `error.code` field in the API's JSON error envelope and the `code` property on every `HatchedError` subclass. @example ```ts import { ErrorCode, HatchedError } from '@hatched/sdk-js'; try { await hatched.events.send({ eventId, userId: 'user_42', type: 'lesson_completed' }); } catch (err) { if (err instanceof HatchedError) { switch (err.code) { case ErrorCode.EventQuotaExceeded: showUpgradeBanner(); break; case ErrorCode.RateLimited: queueRetry(err.retryAfter); break; case ErrorCode.PlanFeatureLocked: hideFeature(); break; default: throw err; } } } ``` Adding new codes here is **additive only** — codes are part of the public contract and never renamed within a major version. If a new code ships, add a key to this object in a minor release. ```ts export const ErrorCode = { // 400 — bad request BadRequest: 'bad_request', InsufficientBalance: 'insufficient_balance', TooManyItems: 'too_many_items', CategoryConflict: 'category_conflict', MissingAudience: 'missing_audience', UnknownAudience: 'unknown_audience', // 422 — validation ValidationFailed: 'validation_failed', // 401 / 403 — auth & scope Unauthorized: 'unauthorized', Forbidden: 'forbidden', PublishableKeyScope: 'publishable_key_scope', WidgetTokenScope: 'widget_token_scope', PlanFeatureLocked: 'plan_feature_locked', CapabilityDisabled: 'capability_disabled', // 402 — billing PaymentRequired: 'payment_required', CreditInsufficient: 'credit_insufficient', EventQuotaExceeded: 'event_quota_exceeded', OnboardingCapReached: 'onboarding_cap_reached', // 404 — not found ResourceNotFound: 'resource_not_found', NotFound: 'not_found', // 409 — conflicts Conflict: 'conflict', ConfigVersionMismatch: 'config_version_mismatch', NoPublishedConfig: 'no_published_config', ActiveEggLimit: 'active_egg_limit', IdempotencyKeyConflict: 'idempotency_key_conflict', // 429 — throttling RateLimited: 'rate_limited', // 502 — upstream UpstreamImageError: 'upstream_image_error', BadGateway: 'bad_gateway', // 503 — service unavailable ServiceUnavailable: 'service_unavailable', OnboardingExtractFailed: 'onboarding_extract_failed', // 500 — internal InternalServerError: 'internal_server_error', } as const; ``` ## Types ### `NoPublishedConfigDetails` ```ts export interface NoPublishedConfigDetails { customerId?: string; customer_id?: string; publishUrl?: string; publish_url?: string; docsUrl?: string; docs_url?: string; } ``` ### `ActiveEggLimitEgg` ```ts export interface ActiveEggLimitEgg { eggId: string; status: string; createdAt: string; } ``` ### `ActiveEggLimitDetails` ```ts export interface ActiveEggLimitDetails { max?: number; active?: ActiveEggLimitWireEgg[]; } ``` ### `CreditInsufficientDetails` ```ts export interface CreditInsufficientDetails { required?: number; available?: number; welcome?: number; paid?: number; promo?: number; upgrade_url?: string; top_up_url?: string; } ``` ### `EventQuotaExceededDetails` ```ts export interface EventQuotaExceededDetails { used?: number; limit?: number; reset_at?: string; upgrade_url?: string; } ``` ### `PlanFeatureLockedDetails` ```ts export interface PlanFeatureLockedDetails { feature?: string; required_plan?: string; current_plan?: string; upgrade_url?: string; } ``` ### `SdkLogger` Pluggable logger for the SDK. The SDK emits four kinds of log lines: - **debug** — Request/response traces. Only emitted when `HatchedClientConfig.debug === true`. - **info** — Reserved; the SDK does not currently emit info lines, but downstream extensions (custom middleware) may. - **warn** — Non-fatal observations the SDK wants the operator to see regardless of `debug`: a literal-looking secret key, a retry hint, an unrecognized rate-limit header. - **error** — Reserved for unrecoverable conditions the SDK is about to throw. Errors are still thrown — logging is in addition. All methods are optional. Provide only the ones you care about; the SDK checks for the method before calling it. This means you can pass a Pino/Winston/Bunyan instance directly without writing an adapter, as long as it has compatible `debug` / `info` / `warn` / `error` methods. @example ```ts import pino from 'pino'; const log = pino({ name: 'hatched' }); const hatched = new HatchedClient({ apiKey: process.env.HATCHED_API_KEY!, logger: log, }); ``` @example ```ts // Minimal "ignore debug, send warnings to Sentry" adapter const hatched = new HatchedClient({ apiKey: process.env.HATCHED_API_KEY!, debug: false, logger: { warn: (msg, fields) => Sentry.captureMessage(msg, { extra: fields }), }, }); ``` ```ts export interface SdkLogger { debug?(message: string, fields?: Record): void; info?(message: string, fields?: Record): void; warn?(message: string, fields?: Record): void; error?(message: string, fields?: Record): void; } ``` ### `FetchLike` ```ts export type FetchLike = (input: string, init?: RequestInit) => Promise; ``` ### `HttpClientConfig` ```ts export interface HttpClientConfig { baseUrl: string; apiKey?: string; publishableKey?: string; widgetToken?: string; timeout: number; maxRetries: number; debug: boolean; fetch?: FetchLike; userAgent: string; logger?: SdkLogger; } ``` ### `RateLimitSnapshot` ```ts export interface RateLimitSnapshot { limit?: number; remaining?: number; reset?: number; retryAfter?: number; } ``` ### `RetryMetadata` Snapshot of what the SDK retried while serving the most recent request. `attempts` is always `1` for a request that succeeded on the first try; `> 1` means at least one retry happened. `reasons` lists the trigger for each retry (`'5xx'`, `'429'`, `'408'`, `'network'`). `totalDelayMs` is the cumulative time spent in backoff waits, not including the requests themselves. The snapshot is overwritten on every request. Read it immediately after the call returns, or pipe it into your tracing/observability layer via the `logger` config. ```ts export interface RetryMetadata { attempts: number; reasons: Array<'5xx' | '429' | '408' | 'network'>; totalDelayMs: number; totalElapsedMs: number; } ``` ### `RequestOptions` ```ts export interface RequestOptions { headers?: Record; idempotent?: boolean; signal?: AbortSignal; /** When false, skips automatic camelCase → snake_case mapping on the body. */ mapCase?: boolean; /** Query parameters (camelCase; converted to snake_case on the wire). */ query?: Record; /** * Whether this endpoint accepts publishable keys. When `false` (default * for mutations) and the client is initialised with a publishable key, * the SDK throws `PublishableKeyScopeError` without a network round-trip. */ allowPublishable?: boolean; /** Whether this endpoint accepts a widget embed/session token. */ allowWidgetToken?: boolean; } ``` ### `EggStatus` ```ts export type EggStatus = 'waiting' | 'ready' | 'hatching' | 'hatched' | 'cancelled'; ``` ### `CreateEggParams` ```ts export interface CreateEggParams { /** The external user id that owns the egg. */ userId: string; /** * Binds the egg — and the buddy it hatches into — to a named audience. * Audience-scoped content (streaks, badges, marketplace items) is keyed by * audience, so a buddy born in the wrong audience makes those widgets 404 or * render empty. Single-audience workspaces can omit this (the server binds the * sole configured audience automatically); multi-audience workspaces must set * it. This is shorthand for `metadata.audience` and takes precedence over it. */ audience?: string; /** Free-form metadata attached to the egg. */ metadata?: Record; /** * When true, return the user's most recent `waiting`/`ready` egg if one * already exists instead of creating a new one (idempotent first-run * bootstrap; avoids hitting the per-user active-egg cap on retries). */ ensure?: boolean; } ``` ### `Egg` ```ts export interface Egg { eggId: string; userId: string; status: EggStatus; visualVariant: number; configVersionId: string; /** The buddy hatched from this egg. Non-null once `status === 'hatched'`. */ buddyId: string | null; metadata: Record; createdAt: string; } ``` ### `EggStatusChange` ```ts export interface EggStatusChange { eggId: string; status: EggStatus; previousStatus: EggStatus; } ``` ### `HatchResult` ```ts export interface HatchResult { operationId: string; status: string; } ``` ### `ListEggsParams` ```ts export interface ListEggsParams { userId?: string; status?: EggStatus; page?: number; limit?: number; signal?: AbortSignal; } ``` ### `AuraTier` ```ts export type AuraTier = 'mythic' | 'legendary' | 'epic' | 'rare' | 'common'; ``` ### `Buddy` ```ts export interface Buddy { id: string; customerId: string; userId: string; audience: string; name: string; configVersionId: string; evolutionStage: number; coins: number; status: 'active' | 'archived'; skills: Record; tokens: Record; progression?: BuddyProgression; imageUrl: string | null; baseImageUrl: string | null; thumbUrl: string | null; equippedItems: BuddyEquippedItem[]; appearance?: BuddyAppearance; auraTier?: AuraTier; /** HTCH-27 — true when the buddy has no equipped items. */ isNaked?: boolean; /** HTCH-105 — F4.4 true when the buddy holds the mentor role. */ isMentor?: boolean; createdAt: string; updatedAt: string; } ``` ### `BuddyProgression` ```ts export interface BuddyProgression { /** Player-facing XP. Currently maps to totalSkillLevel. */ xp: number; totalSkillLevel: number; badgeCount: number; itemCount: number; currentStreak: number; longestStreak: number; customCounters: Record; } ``` ### `BuddyEquippedItem` ```ts export interface BuddyEquippedItem { itemId: string; name: string; imageUrl: string | null; } ``` ### `Badge` ```ts export interface Badge { badgeKey: string; label: string; description: string | null; /** * HTCH-16 user-facing "How to earn" copy. Distinct from `description` * (admin-internal) — the badges widget renders this in the locked-tile * tooltip. Falls back to `description` when null. */ criteriaCopy: string | null; iconUrl: string | null; awardedAt: string; coinReward: number; alreadyAwarded: boolean; } ``` ### `BuddyAppearanceStatus` ```ts export type BuddyAppearanceStatus = 'ready' | 'pending' | 'awaiting_credits' | 'failed'; ``` ### `BuddyAppearance` ```ts export interface BuddyAppearance { status: BuddyAppearanceStatus; operationId: string | null; desiredEquippedItemIds: string[]; renderedEquippedItemIds: string[]; retryable: boolean; message: string | null; error: Record | null; } ``` ### `BuddyListParams` ```ts export interface BuddyListParams { userId?: string; status?: string; evolutionStage?: number; page?: number; limit?: number; sort?: string; order?: 'asc' | 'desc'; signal?: AbortSignal; } ``` ### `SkillUpdate` ```ts export interface SkillUpdate { key: string; action: 'increase' | 'decrease' | 'set'; amount?: number; value?: number; } ``` ### `EarnCoinsParams` ```ts export interface EarnCoinsParams { amount: number; reason: string; referenceId?: string; /** * Token key to earn. Defaults to the customer's `primary` token. Passing * a progression token grants progress; passing any other configured token * key is accepted by the rule engine. Omit to earn the default coin / primary. */ token?: string; } ``` ### `SpendCoinsParams` ```ts export interface SpendCoinsParams { amount: number; reason: string; itemId?: string; /** * Token key to spend. Defaults to the customer's `primary` token. Spending * a progression token fails with ValidationError('progression_not_spendable'). */ token?: string; } ``` ### `TokenBalance` ```ts export interface TokenBalance { /** Canonical `primary` or `progression` identifier. */ kind: 'primary' | 'progression'; /** Customer-defined token key (e.g. `gems`, `xp`). */ key: string; /** Human-readable label for display. */ label: string; /** Current balance. */ balance: number; /** Lifetime earned (earn ledger sum). */ lifetimeEarned: number; /** Lifetime spent (spend ledger sum) — always 0 for progression. */ lifetimeSpent: number; } ``` ### `TokensSummary` ```ts export interface TokensSummary { primary: TokenBalance | null; progression: TokenBalance | null; } ``` ### `BuddyEvolutionRecord` ```ts export interface BuddyEvolutionRecord { id: string; buddyId: string; fromStage: number; toStage: number; triggeredByEventId: string | null; imageUrl: string | null; source: 'prod' | 'demo' | 'auto'; metadata: Record; occurredAt: string; } ``` ### `EquipItemsParams` ```ts export interface EquipItemsParams { equip?: string[]; unequip?: string[]; } ``` ### `EquipItemsResult` ```ts export interface EquipItemsResult { accepted: boolean; operationId: string | null; status: 'pending' | 'completed' | string; appearanceStatus: BuddyAppearanceStatus | string; cached: boolean; } ``` ### `RerenderAppearanceResult` ```ts export interface RerenderAppearanceResult { accepted: boolean; operationId: string; status: 'pending' | string; appearanceStatus: BuddyAppearanceStatus | string; } ``` ### `BuddyList` ```ts export interface BuddyList { data: Buddy[]; meta: { total: number; page: number; limit: number }; } ``` ### `PrestigeBlockReason` Why a buddy cannot prestige right now — F4.3 Prestige Loop. ```ts export type PrestigeBlockReason = | 'disabled' | 'not_max_stage' | 'appearance_pending' | 'cooldown_active' | 'champion_required'; ``` ### `PrestigeAuraTint` The permanent prestige aura ladder. ```ts export type PrestigeAuraTint = 'none' | 'silver' | 'gold' | 'rainbow'; ``` ### `PrestigeStatus` `GET /widget/buddy/prestige` payload — whether the widget buddy can prestige and, when it cannot, the reason. ```ts export interface PrestigeStatus { /** False when the tenant has not enabled the prestige loop. */ available: boolean; canPrestige: boolean; reason: PrestigeBlockReason | null; prestigeLevel: number; evolutionStage: number; maxEvolutionStage: number; /** When `cooldown_active`, the ISO instant the cooldown clears. */ cooldownEndsAt: string | null; } ``` ### `PrestigeResult` `POST /widget/buddy/prestige` success payload. ```ts export interface PrestigeResult { prestigeLevel: number; fromEvolutionStage: number; evolutionStage: number; auraTint: PrestigeAuraTint; prestigedAt: string; seasonId: string | null; /** The buddy's image after the reset (reverted to its clean base). */ imageUrl: string; } ``` ### `SendEventParams` ```ts export interface SendEventParams { /** Stable id used for idempotency. Re-sending the same eventId is a no-op. */ eventId: string; /** External user id the event belongs to. */ userId: string; /** Event type (e.g. `lesson_completed`, `workout_finished`). */ type: string; /** * Audience (role) this event belongs to. Required for customers with 2+ * audiences; omit for single-audience customers and the server applies * the implicit default. Lowercase, snake_case, max 32 chars. */ audience?: string; /** When the event occurred. Defaults to "now" server-side if omitted. */ occurredAt?: Date | string; /** Arbitrary key-value payload forwarded to the rule engine. */ properties?: Record; } ``` ### `EventStreakUpdate` Per-streak progression entry returned alongside coin/badge effects when a tracked event advances a streak. The HTTP client deep-converts snake_case → camelCase, so SDK consumers see camelCase keys here. ```ts export interface EventStreakUpdate { definitionKey: string; label: string; icon: string; current: number; longest: number; milestoneHit: number | null; hero: boolean; } ``` ### `EventPathSubStepCompletion` Per-path completion delta produced when a tracked event closes a sub-step. ```ts export interface EventPathSubStepCompletion { pathKey: string; stepKey: string; subStepKey: string; rewardCoins: number; rewardBadgeKey: string | null; } ``` ### `EventPathStepCompletion` ```ts export interface EventPathStepCompletion { pathKey: string; stepKey: string; rewardCoins: number; rewardBadgeKey: string | null; } ``` ### `EventPathCompletion` ```ts export interface EventPathCompletion { pathKey: string; } ``` ### `EventPathUpdate` ```ts export interface EventPathUpdate { pathKey: string; subStepCompleted?: EventPathSubStepCompletion; stepCompleted?: EventPathStepCompletion; pathCompleted?: EventPathCompletion; } ``` ### `EventEffects` ```ts export interface EventEffects { coins?: number; badgesAwarded?: string[]; badgesReady?: string[]; tokens?: string[]; /** Present when the event was accepted but produced no user-visible effect. */ debugReason?: 'no_active_buddies_for_user' | 'no_matching_rules' | string; /** * True when the buddy has met the next evolution condition. If the * customer's config does not auto-evolve, call `buddies.evolve(buddyId)` * server-side and wait on the returned operation. */ evolutionReady?: boolean; streakMilestones?: number[]; /** Per-streak deltas (current/longest, milestone hits) for active streaks. */ streakUpdates?: EventStreakUpdate[]; /** * Path widget reconciliation deltas. Each entry covers one sub-step * completion plus optional step / path roll-up flags so the host page * can paint celebrations without an extra round-trip. */ pathUpdates?: EventPathUpdate[]; } ``` ### `OperationStatus` ```ts export type OperationStatus = | 'pending' | 'processing' | 'completed' | 'failed' | 'cancelled'; ``` ### `Operation` ```ts export interface Operation { operationId: string; /** Alias for {@link Operation.operationId} for callers that prefer `id`. */ id: string; type: string; status: OperationStatus; result?: TResult; error?: string; createdAt: string; updatedAt: string; } ``` ### `WaitOptions` ```ts export interface WaitOptions { /** Maximum total time to wait, in milliseconds. Default 30_000. */ timeoutMs?: number; /** Poll interval, in milliseconds. Default 2000. */ intervalMs?: number; /** External abort signal. */ signal?: AbortSignal; } ``` ### `PlayerZeroBuddy` ```ts export interface PlayerZeroBuddy { id: string; userId: string; name: string; audience: string; evolutionStage: number; imageUrl: string | null; } ``` ### `PlayerZeroResult` ```ts export interface PlayerZeroResult { /** False when Player Zero already existed and was returned as-is. */ created: boolean; buddy: PlayerZeroBuddy; } ``` ### `PlayerZeroStatus` ```ts export interface PlayerZeroStatus { exists: boolean; /** True once the demo player's hatch ceremony has completed. */ hatched: boolean; buddyId: string | null; } ``` ### `CreateSessionParams` ```ts export interface CreateSessionParams { buddyId: string; userId: string; scopes: string[]; ttlSeconds?: number; } ``` ### `SessionToken` ```ts export interface SessionToken { token: string; sessionId: string; expiresAt: string; scopes: string[]; } ``` ### `CreateEmbedTokenParams` ```ts export interface CreateEmbedTokenParams { buddyId: string; userId: string; ttlSeconds?: number; } ``` ### `EmbedToken` ```ts export interface EmbedToken { token: string; expiresAt: string; mode: 'read-only'; } ``` ### `WebhookEvent` Every webhook event a customer can subscribe to. The canonical source is the API's `WEBHOOK_EVENTS` list in `apps/api/src/webhooks/webhook-events.ts` (the `@IsIn(WEBHOOK_EVENTS)` validation on `CreateWebhookConfigDto`). This union MUST stay in sync with that list — copy names verbatim, do not invent them. Subscribing to a name not in this union is rejected by the API at config-create time. Grouped by domain for readability; entries are sorted alphabetically within each group. ```ts export type WebhookEvent = // appearance | 'appearance.composed' // badge | 'badge.awarded' | 'badge.ready' | 'event_badge.awarded' // beginner's luck | 'beginners_luck.day_3' | 'beginners_luck.day_7_complete' | 'beginners_luck.evaluated' // booster | 'booster.consumed_event' | 'booster.granted' // buddy | 'buddy.aura_tier_changed' | 'buddy.ceremony_completed' | 'buddy.config_migrated' | 'buddy.evolved' | 'buddy.first_outfit_saved' | 'buddy.founding_cohort_awarded' | 'buddy.hatched' | 'buddy.prestiged' // cause (Humanity Hero) | 'cause.threshold_reached' // celebration (White Hat) | 'celebration.milestone_acknowledged' | 'celebration.streak_recovered' // coins | 'coins.earned' | 'coins.spent' // council | 'council.proposal_approved' // egg | 'egg.created' // evolution | 'evolution.hr_forced' | 'evolution.ready' // feed (SeeSaw Bump) | 'feed.team_event_clapped' | 'feed.team_event_created' // flash sale | 'flash_sale.ended' | 'flash_sale.started' // free lunch (Welcome Gift funnel) | 'free_lunch.dismissed' | 'free_lunch.granted' | 'free_lunch.seen' // gate | 'gate.unlocked' // group quest | 'group_quest.joined' | 'group_quest.left' | 'group_quest.missed' | 'group_quest.won' // hexad survey | 'hexad.survey_completed' // item | 'item.equipped' | 'item.gifted' | 'item.purchased' // kudos | 'kudos.received' | 'kudos.sent' // league | 'league.off_season_ended' | 'league.off_season_started' | 'league.season_closed' | 'league.season_ended' | 'league.season_started' | 'league.tier_down' | 'league.tier_up' // lottery | 'lottery.drawn' | 'lottery.entered' | 'lottery.won' // mentor | 'mentor.availability_changed' | 'mentor.badge_threshold_hit' | 'mentor.session_logged' // mystery box | 'mystery_box.opened' // notification | 'notification.created' // operation | 'operation.completed' | 'operation.failed' // outfit (Dress mode) | 'outfit.deleted' | 'outfit.saved' | 'outfit.worn' // path | 'path.completed' | 'path.step_completed' | 'path.sub_step_completed' // recovery (White Hat) | 'recovery.streak_restored' // returning champion | 'returning_champion.crown_equipped' | 'returning_champion.dismissed' | 'returning_champion.shown' // scouting quest | 'scouting_quest.completed' // season challenge | 'season_challenge.completed' // showroom (HR) | 'showroom.award_given' | 'showroom.published' | 'showroom.qr_regenerated' | 'showroom.unpublished' // skill | 'skill.decayed' | 'skill.level_up' | 'skill.updated' // streak | 'streak.at_risk' | 'streak.milestone' // surprise drop | 'surprise_drop.granted' // team | 'team.member_joined' | 'team.member_left' | 'team.role_changed' | 'team_membership.role_upgraded' // token | 'token.earned' | 'token.spent' // usage | 'usage.limit_reached' | 'usage.threshold_reached' // user lifecycle | 'user.lapsed_day_3' | 'user.lapsed_day_7' | 'user.lapsed_day_14' | 'user.welcome_back'; ``` ### `WebhookEndpoint` ```ts export interface WebhookEndpoint { id: string; url: string; events: WebhookEvent[]; active: boolean; status: 'active' | 'paused'; secret?: string; maskedSecret?: string; createdAt: string; updatedAt: string; } ``` ### `CreateWebhookParams` ```ts export interface CreateWebhookParams { url: string; events: WebhookEvent[]; description?: string; } ``` ### `WebhookDelivery` ```ts export interface WebhookDelivery { id: string; endpointId?: string; event: WebhookEvent | string; eventType: WebhookEvent | string; status: 'pending' | 'success' | 'succeeded' | 'failed'; responseCode?: number | null; responseStatus?: number | null; attempt: number; attempts: number; durationMs?: number | null; errorMessage?: string | null; createdAt: string; timestamp: string; lastAttemptAt?: string; } ``` ### `ListDeliveriesParams` ```ts export interface ListDeliveriesParams { endpointId: string; status?: 'pending' | 'success' | 'failed'; cursor?: string; limit?: number; signal?: AbortSignal; } ``` ### `Page` ```ts export interface Page { data: T[]; nextCursor: string | null; } ``` ### `VerifySignatureOptions` ```ts export interface VerifySignatureOptions { /** * The `X-Hatched-Timestamp` header value (unix seconds). Hatched sends the * timestamp in its own header, separate from the signature. Pass it here so * the verifier can recompute the HMAC and enforce the replay window. * * The framework adapters in `@hatched/sdk-js/webhooks` extract this for you. * If omitted, the verifier falls back to a `t=` segment embedded in the * signature header (legacy/combined format) and fails closed if neither is * present. */ timestamp?: string | number; /** Maximum clock-skew in seconds. Defaults to 5 minutes. */ toleranceSeconds?: number; /** Clock used for timestamp validation — useful in tests. */ now?: () => number; } ``` ### `TokenGate` ```ts export interface TokenGate { id: string; gateKey: string; tokenKey: string; cost: number; label: string | null; description: string | null; metadata: Record; isActive: boolean; createdAt: string; updatedAt: string; } ``` ### `UnlockResult` ```ts export interface UnlockResult { gateKey: string; unlocked: true; alreadyUnlocked: boolean; unlockedAt: string; balanceAfter: number | null; metadata: Record; } ``` ### `BuddyUnlock` ```ts export interface BuddyUnlock { gateKey: string; unlockedAt: string; metadata: Record; } ``` ### `PathDisplayMode` ```ts export type PathDisplayMode = 'straight' | 'zigzag' | 'stepper'; ``` ### `PathIcon` ```ts export type PathIcon = 'path' | 'flame' | 'heart' | 'bolt' | 'star' | 'leaf'; ``` ### `PathConditionType` ```ts export type PathConditionType = | 'event_count' | 'milestone' | 'streak' | 'skill_level' | 'collection' | 'evolution' | 'coin' | 'badge_earned' | 'gate_unlocked' | 'custom'; ``` ### `PathCondition` ```ts export interface PathCondition { type: PathConditionType; config: Record; } ``` ### `PathDefinition` ```ts export interface PathDefinition { id: string; customerId: string; audience: string; key: string; label: string; description: string | null; icon: PathIcon; accentColor: string | null; displayMode: PathDisplayMode; isActive: boolean; createdAt: string; updatedAt: string; } ``` ### `PathStep` ```ts export interface PathStep { id: string; pathDefinitionId: string; key: string; label: string; description: string | null; icon: string | null; ordinal: number; unlockCondition: Record | null; completionCondition: PathCondition | null; rewardCoins: number; rewardBadgeKey: string | null; isActive: boolean; createdAt: string; updatedAt: string; } ``` ### `PathSubStep` ```ts export interface PathSubStep { id: string; pathStepId: string; key: string; label: string; description: string | null; ordinal: number; completionCondition: PathCondition | null; allowManualComplete: boolean; allowSkipAhead: boolean; rewardCoins: number; rewardBadgeKey: string | null; contentUrl: string | null; ctaLabel: string | null; isActive: boolean; createdAt: string; updatedAt: string; } ``` ### `PathSubStepStatus` ```ts export type PathSubStepStatus = 'locked' | 'available' | 'completed'; ``` ### `PathSubStepRuntime` ```ts export interface PathSubStepRuntime { id: string; key: string; label: string; description: string | null; ordinal: number; rewardCoins: number; rewardBadgeKey: string | null; contentUrl: string | null; ctaLabel: string | null; allowManualComplete: boolean; allowSkipAhead: boolean; isActive: boolean; status: PathSubStepStatus; completedAt: string | null; } ``` ### `PathStepRuntime` ```ts export interface PathStepRuntime { id: string; key: string; label: string; description: string | null; icon: string | null; ordinal: number; rewardCoins: number; rewardBadgeKey: string | null; isActive: boolean; unlocked: boolean; completed: boolean; subSteps: PathSubStepRuntime[]; } ``` ### `PathRuntimePayload` ```ts export interface PathRuntimePayload { definition: { key: string; label: string; description: string | null; icon: PathIcon; accentColor: string | null; displayMode: PathDisplayMode; }; steps: PathStepRuntime[]; currentStepKey: string | null; completed: boolean; completedAt: string | null; } ``` ### `CreatePathDefinitionParams` ```ts export interface CreatePathDefinitionParams { audience?: string; key: string; label: string; description?: string; icon?: PathIcon; accentColor?: string; displayMode?: PathDisplayMode; isActive?: boolean; } ``` ### `UpdatePathDefinitionParams` ```ts export type UpdatePathDefinitionParams = Partial; ``` ### `CreatePathStepParams` ```ts export interface CreatePathStepParams { key: string; label: string; description?: string; icon?: string; ordinal: number; unlockCondition?: Record; completionCondition?: PathCondition; rewardCoins?: number; rewardBadgeKey?: string; isActive?: boolean; } ``` ### `UpdatePathStepParams` ```ts export type UpdatePathStepParams = Partial; ``` ### `CreatePathSubStepParams` ```ts export interface CreatePathSubStepParams { key: string; label: string; description?: string; ordinal: number; completionCondition?: PathCondition; allowManualComplete?: boolean; allowSkipAhead?: boolean; rewardCoins?: number; rewardBadgeKey?: string; contentUrl?: string; ctaLabel?: string; isActive?: boolean; } ``` ### `UpdatePathSubStepParams` ```ts export type UpdatePathSubStepParams = Partial; ``` ### `ManualCompleteResult` ```ts export interface ManualCompleteResult { alreadyCompleted: boolean; subStepKey: string; stepKey: string; stepCompleted: boolean; pathCompleted: boolean; rewardCoins: number; rewardBadgeKey: string | null; } ``` ### `MarketplaceItemRarity` ```ts export type MarketplaceItemRarity = 'common' | 'rare' | 'epic' | 'legendary' | string; ``` ### `OutfitThumbnailStatus` ```ts export type OutfitThumbnailStatus = 'pending' | 'ready' | 'failed'; ``` ### `OutfitPreviewStatus` ```ts export type OutfitPreviewStatus = 'pending' | 'ready'; ``` ### `CompositionStatus` ```ts export type CompositionStatus = 'pending' | 'ready' | 'failed'; ``` ### `SlotItemMap` ```ts export type SlotItemMap = Record; ``` ### `MarketplaceItem` ```ts export interface MarketplaceItem { id: string; name: string; description: string | null; imageUrl: string | null; category: string; rarity: MarketplaceItemRarity; price: number; isActive: boolean; /** * F2.4 Social Treasure — when true the item can only be received as a gift * from a teammate; a normal purchase is rejected. */ isGiftOnly?: boolean; owned?: boolean; equipped?: boolean; locked?: boolean; lockReason?: string; availableFrom?: string | null; availableUntil?: string | null; } ``` ### `GiftItemParams` ```ts export interface GiftItemParams { /** UUID of the item to gift. */ itemId: string; /** UUID of the teammate buddy that receives the gift. */ toBuddyId: string; /** Optional note delivered with the gift, ≤280 chars. */ message?: string; signal?: AbortSignal; } ``` ### `GiftItemResult` ```ts export interface GiftItemResult { gifted: boolean; /** True when the call hit the 60s accident-click guard — no new debit. */ duplicate: boolean; item_id: string; from_buddy_id: string; to_buddy_id: string; price_paid: number; /** Sender's coin balance after the debit. */ remaining_coins: number; /** The receiver-side item purchase row id. */ purchase_id: string; message: string | null; gifted_at: string; } ``` ### `MarketplaceListParams` ```ts export interface MarketplaceListParams { category?: string; rarity?: string; page?: number; limit?: number; signal?: AbortSignal; } ``` ### `MarketplaceListResponse` ```ts export interface MarketplaceListResponse { data: MarketplaceItem[]; pagination: { page: number; limit: number; total: number; totalPages: number; }; } ``` ### `OutfitSummary` ```ts export interface OutfitSummary { id: string; buddyId: string; customerId: string; name: string; slotItemMap: SlotItemMap; isActive: boolean; thumbnailVariantId: string | null; thumbnailStatus: OutfitThumbnailStatus; thumbnailUrl?: string | null; createdAt: string; updatedAt: string; } ``` ### `OutfitListResponse` ```ts export interface OutfitListResponse { outfits: OutfitSummary[]; } ``` ### `OutfitPreviewResponse` ```ts export interface OutfitPreviewResponse { variantId: string | null; status: OutfitPreviewStatus; imageUrl: string | null; } ``` ### `CompositionStatusResponse` ```ts export interface CompositionStatusResponse { status: CompositionStatus; variantUrl: string | null; error: string | null; } ``` ### `SaveOutfitParams` ```ts export interface SaveOutfitParams { name: string; slotItemMap: SlotItemMap; } ``` ### `ActivateOutfitResponse` ```ts export interface ActivateOutfitResponse { outfit: OutfitSummary; equip: unknown; } ``` ### `DeleteOutfitResponse` ```ts export interface DeleteOutfitResponse { deleted: true; } ``` ### `BadgeProgress` ```ts export interface BadgeProgress { current: number; target: number; } ``` ### `WidgetBadge` ```ts export interface WidgetBadge { id: string; key?: string; name: string; iconUrl: string | null; description: string | null; earnedAt: string | null; criteriaCopy: string; progress?: BadgeProgress; } ``` ### `EarnedWidgetBadge` ```ts export interface EarnedWidgetBadge { badgeKey: string; label: string; description: string | null; criteriaCopy?: string | null; iconUrl: string | null; awardedAt: string; coinReward: number; alreadyAwarded: boolean; } ``` ### `LockedWidgetBadge` ```ts export interface LockedWidgetBadge { key: string; label: string; description: string | null; criteriaCopy?: string | null; iconUrl: string | null; coinReward: number; conditionType: string; progress: { current: number; required: number } | null; hint: string | null; } ``` ### `BadgeListParams` ```ts export interface BadgeListParams { includeLocked?: boolean; include_locked?: boolean; signal?: AbortSignal; } ``` ### `BadgeListResponse` ```ts export interface BadgeListResponse { badges: WidgetBadge[]; earned: EarnedWidgetBadge[]; locked: LockedWidgetBadge[]; total?: number; } ``` ### `LeaderboardMetric` ```ts export type LeaderboardMetric = | 'total_xp' | 'coins' | 'badge_count' | 'evolution_stage' | 'badges' | 'evolution' | 'skills'; ``` ### `LeaderboardPeriod` ```ts export type LeaderboardPeriod = 'daily' | 'weekly' | 'monthly' | 'all_time'; ``` ### `LeaderboardViewMode` ```ts export type LeaderboardViewMode = 'top' | 'around_me' | 'hybrid'; ``` ### `LeaderboardScope` ```ts export type LeaderboardScope = 'global' | 'team'; ``` ### `LeaderboardScopeParam` Request-side scope filter for `leaderboard.get`. ```ts export type LeaderboardScopeParam = LeaderboardScope; ``` ### `LeaderboardEntry` ```ts export interface LeaderboardEntry { rank: number; buddyId: string; userId: string; name: string; imageUrl: string | null; thumbUrl: string | null; evolutionStage: number; score: number; auraTier: AuraTier; } ``` ### `LeaderboardUserEntry` ```ts export interface LeaderboardUserEntry { rank: number; buddyId: string; name: string; score: number; } ``` ### `LeaderboardUrgentOptimism` ```ts export interface LeaderboardUrgentOptimism { rankAbove: number; name: string; scoreGap: number; } ``` ### `LeaderboardWindow` ```ts export interface LeaderboardWindow { size: number; currentUserRank: number | null; } ``` ### `LeaderboardScopePolicy` ```ts export interface LeaderboardScopePolicy { allowOverride: boolean; teamLabel: string | null; globalLabel: string | null; defaultScope: LeaderboardScope; } ``` ### `LeaderboardResponse` ```ts export interface LeaderboardResponse { entries: LeaderboardEntry[]; topStrip: LeaderboardEntry[] | null; urgentOptimism: LeaderboardUrgentOptimism | null; currentUserRank?: number; currentUser?: LeaderboardUserEntry; userRank: number | null; userEntry: LeaderboardUserEntry | null; viewMode: LeaderboardViewMode; window: LeaderboardWindow; metric: 'total_xp' | 'coins' | 'badge_count' | 'evolution_stage'; period: LeaderboardPeriod; totalEntries: number; /** HTCH-48: resolved scope after applying tenant default + override. */ effectiveScope?: LeaderboardScope; /** HTCH-48: team id actually filtered on (null unless scope='team'). */ effectiveTeamId?: string | null; /** HTCH-21: scope-toggle policy + admin label overrides. */ scopePolicy?: LeaderboardScopePolicy; } ``` ### `LeaderboardParams` ```ts export interface LeaderboardParams { metric?: LeaderboardMetric; period?: LeaderboardPeriod; limit?: number; viewMode?: LeaderboardViewMode; aroundMe?: boolean; around_me?: boolean; topStripSize?: number; top_strip_size?: number; /** * 'team' restricts to the requesting buddy's active team roster, 'global' * lifts the filter. */ scope?: LeaderboardScopeParam; signal?: AbortSignal; } ``` ### `NextBestActionKind` ```ts export type NextBestActionKind = | 'streak_warning' | 'almost_badge' | 'level_up_close' | 'group_quest_invite' | 'kudos_owed' | 'dress_first_outfit' | 'try_new_outfit' | 'fill_cohort'; ``` ### `NextBestAction` ```ts export interface NextBestAction { kind: NextBestActionKind; copy: string; priority: number; targetWidget?: string; targetUrl?: string; } ``` ### `NextBestActionResponse` ```ts export interface NextBestActionResponse { action: NextBestAction | null; fallbackUsed: boolean; } ``` ### `TeamRole` ```ts export type TeamRole = 'member' | 'lead' | 'mentor'; ``` ### `TeamMember` ```ts export interface TeamMember { buddy_id: string; display_name: string; avatar_url: string | null; role: TeamRole; /** F2.6 mentor availability flag — false until Mentor visibility ships. */ mentor_available: boolean; } ``` ### `Team` ```ts export interface Team { id: string; name: string; slug: string; avatar_url: string | null; member_count: number; } ``` ### `MyTeamResponse` ```ts export interface MyTeamResponse { team: Team | null; role: TeamRole | null; members: TeamMember[]; } ``` ### `SendKudosParams` ```ts export interface SendKudosParams { toBuddyId: string; kudoTypeKey: string; /** Optional note, ≤280 chars. Required when the workspace enforces it. */ message?: string; signal?: AbortSignal; } ``` ### `SentKudos` ```ts export interface SentKudos { id: string; from_buddy_id: string; to_buddy_id: string; kudo_type_key: string; weight: number; message: string | null; created_at: string; /** True when the call hit the 60s accident-click guard — no new kudos. */ duplicate: boolean; daily_cap: number; sent_today: number; } ``` ### `KudosFeedEntry` ```ts export interface KudosFeedEntry { id: string; /** The counterparty buddy — sender on a received kudos, receiver on a given one. */ buddy_id: string; display_name: string; avatar_url: string | null; kudo_type_key: string; kudo_type_label: string; kudo_type_icon: string | null; weight: number; message: string | null; created_at: string; } ``` ### `GivenKudosResponse` ```ts export interface GivenKudosResponse { kudos: KudosFeedEntry[]; /** Lifetime count of kudos this buddy has sent — the assist metric. */ total: number; } ``` ### `KudoTypeView` ```ts export interface KudoTypeView { /** null for the virtual generic fallback (not yet persisted). */ id: string | null; key: string; label: string; icon: string | null; description: string | null; weight: number; is_active: boolean; sort_order: number; is_virtual: boolean; } ``` ### `ActiveGroupQuest` An active Group Quest as seen by an embedded buddy (widget-token surface). ```ts export interface ActiveGroupQuest { id: string; title: string; description: string | null; target_metric: string; target_metric_label: string; target_value: number; current_value: number; deadline: string; min_participants: number; participants_count: number; /** 'team' = scoped to the buddy's team; 'workspace' = customer-wide. */ team_scope: 'team' | 'workspace'; /** True when the requesting buddy is already a participant. */ joined: boolean; /** Compact reward chip label, e.g. "🎁 +500 coins". */ reward_summary: string; } ``` ### `JoinGroupQuestResult` ```ts export interface JoinGroupQuestResult { joined: boolean; /** True when the buddy was already a participant — the call was a no-op. */ already_joined: boolean; participants_count: number; } ``` ### `LeaveGroupQuestResult` ```ts export interface LeaveGroupQuestResult { left: boolean; participants_count: number; } ``` ### `GroupQuestStatus` ```ts export type GroupQuestStatus = | 'draft' | 'active' | 'won' | 'missed' | 'cancelled'; ``` ### `GroupQuestRewardConfig` Reward distributed to every participant when a quest is won. ```ts export interface GroupQuestRewardConfig { economy_grant?: number; item_grant?: string; badge_grant?: string; } ``` ### `AdminGroupQuest` A Group Quest as seen on the tenant admin (Planner) surface. ```ts export interface AdminGroupQuest { id: string; team_id: string | null; title: string; description: string | null; target_metric: string; target_value: number; current_value: number; deadline: string; min_participants: number; reward_config: GroupQuestRewardConfig; participants_count: number; status: GroupQuestStatus; created_at: string; updated_at: string; completed_at: string | null; } ``` ### `CreateGroupQuestParams` ```ts export interface CreateGroupQuestParams { title: string; description?: string | null; targetMetric: string; targetValue: number; /** ISO 8601 — must be more than 1 hour in the future. */ deadline: string; minParticipants?: number; rewardConfig: GroupQuestRewardConfig; /** Null / omitted = customer-wide quest. */ teamId?: string | null; } ``` ### `UpdateGroupQuestParams` ```ts export interface UpdateGroupQuestParams { title?: string; description?: string | null; deadline?: string; minParticipants?: number; targetValue?: number; rewardConfig?: GroupQuestRewardConfig; /** Set to 'cancelled' to end an active quest. */ status?: 'cancelled'; } ``` ### `ForceResolveResult` ```ts export interface ForceResolveResult { status: 'won' | 'missed'; current_value: number; target_value: number; participants_count: number; reward_summary: { economy_per_buddy: number | null; item_id: string | null; badge_key: string | null; } | null; } ``` ### `MentorDirectoryEntry` A team member open to mentoring, with a server-rendered contact deep link. ```ts export interface MentorDirectoryEntry { buddy_id: string; display_name: string; avatar_url: string | null; /** Resolved contact URL, or null when the workspace has no template. */ contact_url: string | null; } ``` ### `MentorSession` ```ts export interface MentorSession { id: string; /** Quarter-hour granularity, 0.25–8. */ hours: number; mentee_label: string | null; summary: string | null; logged_at: string; } ``` ### `MentorSessionsResponse` ```ts export interface MentorSessionsResponse { sessions: MentorSession[]; total_hours_all_time: number; total_hours_this_season: number; } ``` ### `LogSessionParams` ```ts export interface LogSessionParams { /** Single-session hours, 0.25–8, quarter-hour steps. */ hours: number; /** Optional mentee name / project label, ≤80 chars. */ menteeLabel?: string; /** Optional session note, ≤280 chars. */ summary?: string; signal?: AbortSignal; } ``` ### `LogSessionResult` ```ts export interface LogSessionResult { session: MentorSession; total_hours_all_time: number; /** True when this log crossed the threshold and earned the Mentor badge. */ badge_awarded: boolean; } ``` ### `BragEventKind` The Win-State that surfaced the Brag Button (HTCH-60). ```ts export type BragEventKind = | 'evolution' | 'hatch_complete' | 'badge_awarded' | 'league_promotion' | 'kudos_received_high_weight' | 'group_quest_won'; ``` ### `BragChannel` A share destination the Brag Button can target. ```ts export type BragChannel = | 'linkedin' | 'slack' | 'teams' | 'twitter' | 'web_share'; ``` ### `BragWebhookChannel` Channels a Slack/Teams webhook post can be sent to. ```ts export type BragWebhookChannel = 'slack' | 'teams'; ``` ### `BragTelemetryAction` One step of the brag share funnel. ```ts export type BragTelemetryAction = | 'opened' | 'clicked' | 'completed' | 'dismissed'; ``` ### `RecordBragTelemetryParams` ```ts export interface RecordBragTelemetryParams { /** The Win-State that surfaced the Brag Button. */ eventKind: BragEventKind; /** The share destination the funnel step relates to. */ channel: BragChannel; /** The funnel step being recorded. */ action: BragTelemetryAction; signal?: AbortSignal; } ``` ### `SendBragSlackPostParams` ```ts export interface SendBragSlackPostParams { /** The Win-State being bragged about. */ eventKind: BragEventKind; /** Webhook channel — `slack` or `teams`. */ channel: BragWebhookChannel; /** The user-edited post body (≤1000 chars). */ customMessage: string; signal?: AbortSignal; } ``` ### `BragSlackPostResult` ```ts export interface BragSlackPostResult { /** Always true on success — a non-2xx webhook throws `webhook_failed`. */ delivered: true; } ``` ### `BragTelemetryResult` ```ts export interface BragTelemetryResult { recorded: true; } ``` ### `TeamEventKind` The kinds of team event the SeeSaw Bump feed surfaces (HTCH-63). ```ts export type TeamEventKind = | 'streak_milestone' | 'group_quest_won' | 'group_quest_missed' | 'evolution' | 'badge_awarded_rare' | 'kudos_received_high_weight' | 'first_outfit_saved' | 'mentor_hours_milestone'; ``` ### `TeamEvent` ```ts export interface TeamEvent { id: string; event_kind: TeamEventKind; subject_buddy_id: string; subject_name: string; subject_avatar_url: string | null; /** Server-rendered display copy — positive framing only. */ copy_rendered: string; payload: Record; clap_count: number; /** True when the requesting buddy has clapped this event. */ i_clapped: boolean; created_at: string; } ``` ### `TeamEventsPage` ```ts export interface TeamEventsPage { events: TeamEvent[]; /** Opaque cursor for the next page, or null when the feed is exhausted. */ next_cursor: string | null; } ``` ### `ListTeamEventsParams` ```ts export interface ListTeamEventsParams { /** Opaque cursor from a previous page's `next_cursor`. */ cursor?: string; /** Page size — clamped server-side to the tenant's max. */ limit?: number; signal?: AbortSignal; } ``` ### `ClapResult` ```ts export interface ClapResult { team_event_id: string; clap_count: number; /** True when the buddy now holds a clap, false after an unclap. */ i_clapped: boolean; } ``` ### `SocialNormFraming` The three positive framings a social norm may take (HTCH-62). ```ts export type SocialNormFraming = | 'positive_completion' | 'elitism_first_n' | 'momentum_today'; ``` ### `SocialNorm` ```ts export interface SocialNorm { /** Stable catalog key. */ key: string; /** The copy template (with `{{placeholders}}`), tenant override applied. */ copy_template: string; /** Server-rendered copy, ready to display — positive framing only. */ copy_rendered: string; framing: SocialNormFraming; /** The metric value (percent or count) the copy renders. */ metric_value: number; /** Denominator for count framings, null for percent/scarcity. */ total: number | null; /** ISO timestamp — end of the UTC day the norm is valid for. */ expires_at: string; } ``` ### `SocialNormsTodayResponse` ```ts export interface SocialNormsTodayResponse { norms: SocialNorm[]; } ``` ### `CauseTriggerEvent` Rule-engine event types a cause may count. ```ts export type CauseTriggerEvent = | 'skill.completed' | 'badge.awarded' | 'kudos.sent' | 'level_up' | 'hatch.completed'; ``` ### `AdminCause` A tenant-defined symbolic cause ("Trees planted") as seen on the admin (Planner) surface. `webhookUrl` is a Phase-2 placeholder — always null until the Phase-4 Humanity Hero integration (F4.5). ```ts export interface AdminCause { id: string; key: string; label: string; icon: string | null; unit: string; /** Every N eligible events advance the symbolic counter by one unit. */ rate: number; trigger_events: CauseTriggerEvent[]; webhook_url: string | null; enabled: boolean; created_at: string; updated_at: string; } ``` ### `CreateCauseParams` ```ts export interface CreateCauseParams { key: string; label: string; icon?: string | null; unit: string; /** Defaults to 1 (every event = 1 unit). */ rate?: number; triggerEvents: CauseTriggerEvent[]; /** Defaults to false — the believability gate is opt-in. */ enabled?: boolean; } ``` ### `UpdateCauseParams` ```ts export interface UpdateCauseParams { label?: string; icon?: string | null; unit?: string; rate?: number; triggerEvents?: CauseTriggerEvent[]; enabled?: boolean; } ``` ### `CausePreview` Projected symbolic units from the last 30 days of eligible events. ```ts export interface CausePreview { event_count: number; rate: number; projected_units: number; window_days: number; } ``` ### `FoundingCohortMode` How the Founding Cohort decides which buddies count as early joiners. ```ts export type FoundingCohortMode = | 'first_n' | 'first_percent' | 'first_until_date'; ``` ### `FoundingCohortPreview` Projection of how many buddies the current cohort config would mark. ```ts export interface FoundingCohortPreview { enabled: boolean; mode: FoundingCohortMode; eligible_count: number; ineligible_count: number; already_member_count: number; total_buddies: number; /** False when `first_percent` falls back to the live buddy count. */ uses_expected_total: boolean; sample_buddies: Array<{ id: string; name: string; created_at: string }>; } ``` ### `FoundingCohortBackfillResult` ```ts export interface FoundingCohortBackfillResult { awarded_count: number; total_members: number; scanned: number; } ``` ### `FoundingCohortAuditEntry` ```ts export interface FoundingCohortAuditEntry { id: string; buddy_id: string; source: 'real_time' | 'backfill'; awarded_at: string; title_label: string | null; } ``` ### `FlashSaleStatus` ```ts export type FlashSaleStatus = | 'scheduled' | 'running' | 'ended' | 'cancelled'; ``` ### `FlashSaleSelectionMode` ```ts export type FlashSaleSelectionMode = 'random' | 'curated'; ``` ### `FlashSale` A marketplace flash sale as seen on the tenant admin (Planner) surface. ```ts export interface FlashSale { id: string; customer_id: string; name: string; starts_at: string; duration_minutes: number; discount_percent: number; item_selection_mode: FlashSaleSelectionMode; curated_item_ids: string[] | null; status: FlashSaleStatus; affected_item_ids: string[] | null; ends_at: string | null; created_at: string; updated_at: string; } ``` ### `ScheduleFlashSaleParams` ```ts export interface ScheduleFlashSaleParams { name: string; /** ISO 8601 — must be a future time. */ startsAt: string; /** 5–1440 minutes. Defaults to 60. */ durationMinutes?: number; /** 1–90 percent. Defaults to 50. */ discountPercent?: number; /** `random` lets the cron pick 5 items; `curated` uses `curatedItemIds`. */ itemSelectionMode?: FlashSaleSelectionMode; /** Required (non-empty) when `itemSelectionMode` is `curated`. */ curatedItemIds?: string[]; signal?: AbortSignal; } ``` ### `LotteryCadence` ```ts export type LotteryCadence = 'weekly' | 'monthly'; ``` ### `LotteryEligibilityRule` When a buddy is entered: M events of a type within a rolling N-day window. ```ts export interface LotteryEligibilityRule { /** Event type that counts — `*` matches every ingested event. */ event_type: string; min_count: number; window_days: number; } ``` ### `LotteryPrizeTier` One prize tier — `winners_count` buddies share it. ```ts export interface LotteryPrizeTier { type: 'item' | 'coins' | 'badge'; /** Item key/id or badge key — required for `item` and `badge`. */ ref?: string | null; /** Coin amount — required for `coins`. */ amount?: number | null; winners_count: number; } ``` ### `LotteryDrawSchedule` Structured draw schedule — wall-clock time in the definition timezone. ```ts export interface LotteryDrawSchedule { /** 0=Sun..6=Sat — weekly cadence. */ day_of_week?: number; /** 1..28 — monthly cadence. */ day_of_month?: number; hour: number; minute: number; } ``` ### `LotteryDefinition` A lottery definition on the tenant admin surface. ```ts export interface LotteryDefinition { id: string; customer_id: string; name: string; eligibility_rule: LotteryEligibilityRule; cadence: LotteryCadence; prize_pool: LotteryPrizeTier[]; draw_at: LotteryDrawSchedule; tz: string; active: boolean; created_at: string; updated_at: string; } ``` ### `LotteryResolvedPrize` A prize tier resolved onto a specific winning entry. ```ts export interface LotteryResolvedPrize { buddy_id: string; entry_id: string; prize: LotteryPrizeTier; } ``` ### `LotteryDraw` A resolved period draw. ```ts export interface LotteryDraw { id: string; lottery_definition_id: string; period_key: string; period_start: string; period_end: string; drawn_at: string; winner_entry_ids: string[]; total_entries: number; prize_resolved: LotteryResolvedPrize[]; seed: string; } ``` ### `LotteryNextDrawPreview` The live "Next draw" preview card. ```ts export interface LotteryNextDrawPreview { current_period_key: string; current_period_entries: number; next_draw_at: string; } ``` ### `SimulatedLotteryDraw` A non-persisted simulated draw. ```ts export interface SimulatedLotteryDraw { total_entries: number; seed: string; winners: LotteryResolvedPrize[]; } ``` ### `CreateLotteryParams` ```ts export interface CreateLotteryParams { name: string; eligibilityRule: LotteryEligibilityRule; cadence: LotteryCadence; prizePool: LotteryPrizeTier[]; drawAt: LotteryDrawSchedule; /** IANA timezone for the draw schedule. Defaults to `UTC`. */ tz?: string; active?: boolean; signal?: AbortSignal; } ``` ### `UpdateLotteryParams` ```ts export type UpdateLotteryParams = Partial; ``` ### `EventBadgeCondition` The eligibility condition a campaign checks against the triggering event. `logged_in_during_window` — any activity inside the window earns the badge (the broadest mode); `event_type` — only a specific event type earns it. ```ts export type EventBadgeCondition = | { type: 'logged_in_during_window' } | { type: 'event_type'; event_type: string }; ``` ### `EventBadgeNarrativeCallout` Bilingual reveal copy shown in the badge tooltip. ```ts export interface EventBadgeNarrativeCallout { tr?: string; en?: string; } ``` ### `EventBadgeDefinition` An event-triggered badge campaign as returned by create / update. ```ts export interface EventBadgeDefinition { id: string; customer_id: string; badge_key: string; trigger_window_start: string; trigger_window_end: string; condition: EventBadgeCondition; narrative_callout: EventBadgeNarrativeCallout; enabled: boolean; created_at: string; updated_at: string; } ``` ### `EventBadgeCampaign` A campaign enriched with the badge label and its grant count (list view). ```ts export interface EventBadgeCampaign extends EventBadgeDefinition { badge_name: string | null; grant_count: number; } ``` ### `CreateEventBadgeParams` ```ts export interface CreateEventBadgeParams { /** Badge key — resolved per-buddy by audience + config version at grant time. */ badgeKey: string; /** ISO-8601 — the campaign window opens. */ triggerWindowStart: string; /** ISO-8601 — the campaign window closes. Must be after the start. */ triggerWindowEnd: string; /** Defaults to `{ type: 'logged_in_during_window' }`. */ condition?: EventBadgeCondition; narrativeCallout?: EventBadgeNarrativeCallout; enabled?: boolean; signal?: AbortSignal; } ``` ### `UpdateEventBadgeParams` ```ts export type UpdateEventBadgeParams = Partial; ``` ### `ProfileSlot` The fixed layout slots a profile page composes into. ```ts export type ProfileSlot = 'header' | 'hero' | 'left' | 'right' | 'footer'; ``` ### `WidgetSpec` One placed widget — a catalog key plus its props. ```ts export interface WidgetSpec { widget_key: string; props: Record; } ``` ### `ProfileTemplateLayout` A full profile layout — a versioned widget tree keyed by slot. ```ts export interface ProfileTemplateLayout { version: number; slots: Record; } ``` ### `ProfileTemplateSource` The built-in starting point a custom template was forked from. ```ts export type ProfileTemplateSource = | 'minimal' | 'showcase_heavy' | 'mission_driven' | 'showcase_mentor' | 'custom'; ``` ### `ProfileTemplate` A tenant-authored profile-page template. ```ts export interface ProfileTemplate { id: string; customer_id: string | null; name: string; source_template: ProfileTemplateSource; layout: ProfileTemplateLayout; theme_overrides: Record | null; is_default: boolean; created_at: string; updated_at: string; } ``` ### `SystemProfileTemplate` A built-in system template — global, forkable, never edited in place. ```ts export interface SystemProfileTemplate { source_template: Exclude; name: string; description: string; layout: ProfileTemplateLayout; } ``` ### `ProfileTemplateList` The gallery payload: code-defined system templates + the tenant's own. ```ts export interface ProfileTemplateList { system: SystemProfileTemplate[]; custom: ProfileTemplate[]; } ``` ### `CreateProfileTemplateParams` ```ts export interface CreateProfileTemplateParams { name: string; /** The built-in starting point this template was forked from. */ sourceTemplate: ProfileTemplateSource; /** The widget composition — `{ version, slots }`. Normalized server-side. */ layout: ProfileTemplateLayout; themeOverrides?: Record | null; /** Make this the tenant default — unsets any previous default. */ isDefault?: boolean; signal?: AbortSignal; } ``` ### `UpdateProfileTemplateParams` ```ts export type UpdateProfileTemplateParams = Partial< Omit >; ``` ### `BulkApplyTarget` Who a bulk-apply targets — all buddies, or a single audience. ```ts export type BulkApplyTarget = | { type: 'all' } | { type: 'audience'; audience: string }; ``` ### `BulkApplyProfileTemplateParams` ```ts export interface BulkApplyProfileTemplateParams { /** The template to assign. */ templateId: string; target: BulkApplyTarget; signal?: AbortSignal; } ``` ### `LeagueWidgetUnavailableReason` Why the league widget has nothing to show. ```ts export type LeagueWidgetUnavailableReason = | 'capability_off' | 'no_active_season' | 'not_enrolled'; ``` ### `LeagueTierView` A tier rendered in the widget — banner colour + the next-tier target. ```ts export interface LeagueTierView { id: string; name: string; colorHex: string; iconKey: string; order: number; } ``` ### `LeagueStandingRow` One row of the cohort standings the widget renders. ```ts export interface LeagueStandingRow { buddyId: string; name: string; imageUrl: string | null; points: number; rank: number; isSelf: boolean; } ``` ### `LeagueSelfStanding` The buddy's own season summary. ```ts export interface LeagueSelfStanding { buddyId: string; name: string; points: number; rank: number; /** True when the buddy sits in the bottom quarter of the cohort. */ inDemotionZone: boolean; } ``` ### `LeagueSeasonView` The active season the buddy is competing in. ```ts export interface LeagueSeasonView { id: string; seasonNumber: number; name: string | null; scheduledEndAt: string; } ``` ### `LeagueWidgetSnapshot` The `GET /widget/leagues/me` payload. `available: false` is a normal outcome (plan not entitled, no active season, or the buddy not enrolled) — callers render nothing rather than treating it as an error. ```ts export interface LeagueWidgetSnapshot { available: boolean; reason: LeagueWidgetUnavailableReason | null; tier: LeagueTierView | null; /** The tier one rung up — the promotion target. Null at the top tier. */ nextTier: LeagueTierView | null; season: LeagueSeasonView | null; cohortSize: number; /** Top 5 of the cohort, plus the buddy's own row pinned if outside the top 5. */ standings: LeagueStandingRow[]; self: LeagueSelfStanding | null; /** Advisory promotion threshold for the progress bar. */ minPointsForPromotion: number; } ``` ### `LeagueSeasonHighlightKind` Which kind of season-closing highlight a scene represents. ```ts export type LeagueSeasonHighlightKind = | 'best_week' | 'most_kudos_sent' | 'most_items_collected' | 'cohort_role'; ``` ### `LeagueSeasonHighlight` One personalized season-closing highlight scene. ```ts export interface LeagueSeasonHighlight { kind: LeagueSeasonHighlightKind; title: string; value: number; detail: string; /** Standardised score within the cohort — the highest is the Brag pick. */ zScore: number; } ``` ### `LeagueSeasonHighlightsSnapshot` The `GET /widget/leagues/seasons/:id/highlights/me` payload. ```ts export interface LeagueSeasonHighlightsSnapshot { available: boolean; seasonId: string; seasonNumber: number; highlights: LeagueSeasonHighlight[]; topHighlightKind: LeagueSeasonHighlightKind | null; outcome: 'promoted' | 'stayed' | 'demoted'; finalRank: number; cohortSize: number; shareCode: string | null; } ``` ### `BossFightTargetMetric` The metric a Boss Fight counts toward its season-long target. ```ts export type BossFightTargetMetric = | 'skill_events' | 'kudos_sent' | 'items_collected'; ``` ### `BossFightCopy` A Boss Fight's name or description copy (English-only). ```ts export type BossFightCopy = string; ``` ### `BossFightLeaderRow` One row of the Boss Fight challenge leaderboard. ```ts export interface BossFightLeaderRow { buddyId: string; name: string; imageUrl: string | null; progressValue: number; /** Set once the buddy clears the boss, in completion order (1 = first). */ completedRank: number | null; isSelf: boolean; } ``` ### `BossFightProgressView` The `GET /widget/leagues/boss-fight` payload — F4.2 Seasonal Challenge. `available: false` is a normal outcome (no active season, no boss fight on it, or the buddy not enrolled) — callers render nothing. ```ts export interface BossFightProgressView { available: boolean; seasonId: string | null; seasonNumber: number | null; name: BossFightCopy | null; description: BossFightCopy | null; targetMetric: BossFightTargetMetric | null; targetValue: number; /** The requesting buddy's accumulated progress. */ selfProgress: number; /** The buddy's completion rank, or null if the boss is not yet cleared. */ selfCompletedRank: number | null; /** When the boss-fight season closes (ISO) — the challenge deadline. */ endsAt: string | null; /** Top finishers by progress. */ leaderboard: BossFightLeaderRow[]; } ``` ### `HexadType` ```ts export type HexadType = | 'philanthropist' | 'socialiser' | 'free_spirit' | 'achiever' | 'player' | 'disruptor'; ``` ### `HexadSurveyQuestion` A single survey question as advertised to the widget. ```ts export interface HexadSurveyQuestion { /** Stable key — `q1..q24` in the default catalog. */ key: string; /** Which Hexad axis the answer feeds. */ axis: HexadType; /** When true, the scorer flips the Likert direction (1↔5). */ reverse?: boolean; } ``` ### `HexadSurveyQuestionsResponse` ```ts export interface HexadSurveyQuestionsResponse { /** Current consent version — re-stamp by submitting a fresh response. */ consent_version: string; questions: HexadSurveyQuestion[]; } ``` ### `HexadAnswerMap` Map of `questionKey -> Likert (1..5)`. ```ts export interface HexadAnswerMap { [questionKey: string]: number; } ``` ### `SubmitHexadSurveyParams` ```ts export interface SubmitHexadSurveyParams { answers: HexadAnswerMap; /** Consent version the buddy is explicitly acknowledging. */ consentVersion: string; signal?: AbortSignal; } ``` ### `HexadSurveySubmitResult` ```ts export interface HexadSurveySubmitResult { primary_type: HexadType; derived_scores: Record; consent_version: string; /** ISO timestamp — raw row is purged on/after this date. */ retention_expires_at: string; } ``` ### `HexadSurveyMyResponse` ```ts export interface HexadSurveyMyResponse { response: { primary_type: HexadType; derived_scores: Record; consent_version: string; retention_expires_at: string; responded_at: string; } | null; } ``` ### `AuthMethod` ```ts export type AuthMethod = 'api_key' | 'dashboard_jwt'; ``` ### `ApiKeyType` ```ts export type ApiKeyType = 'secret' | 'publishable'; ``` ### `PlanKey` ```ts export type PlanKey = 'starter' | 'growth' | 'pro' | 'enterprise'; ``` ### `ApiKeySummary` ```ts export interface ApiKeySummary { id: string; label: string | null; keyType: ApiKeyType; scopes: string[]; lastUsedAt: string | null; createdAt: string; } ``` ### `WhoamiResult` ```ts export interface WhoamiResult { customerId: string; customerName: string; plan: PlanKey; /** Capability flags currently entitled by the customer's plan. */ capabilities: string[]; /** Whether the caller authenticated with an API key or a dashboard JWT. */ authMethod: AuthMethod; /** Null when authenticating with a dashboard JWT. */ apiKey: ApiKeySummary | null; workspaceRef: string; } ``` ### `Notification` One in-app notification, as the banner widget renders it. ```ts export interface Notification { id: string; /** The emitter's notification type key (e.g. `streak_watchdog`). */ type: string; /** Display severity — drives the banner's accent treatment. */ severity: string; title: string; body: string; /** Icon key the widget maps to its glyph set. */ icon: string; ctaUrl: string | null; ctaLabel: string | null; /** When the notification self-expires, or null when it never does. */ expiresAt: string | null; readAt: string | null; dismissedAt: string | null; metadata: Record; createdAt: string; } ``` ### `NotificationFeedPage` ```ts export interface NotificationFeedPage { notifications: Notification[]; /** Opaque cursor for the next page, or null when the feed is exhausted. */ nextCursor: string | null; /** Unread, non-dismissed count — folded in so the badge needs one poll. */ unreadCount: number; /** When the buddy's vacation pause lifts, or null when not paused. */ pausedUntil: string | null; } ``` ### `ListNotificationsParams` ```ts export interface ListNotificationsParams { /** Opaque cursor from a previous page's `nextCursor`. */ cursor?: string; /** Page size — clamped server-side. */ limit?: number; /** Include already-read notifications in the feed (default false). */ includeRead?: boolean; signal?: AbortSignal; } ``` ### `UnreadCountResult` ```ts export interface UnreadCountResult { unreadCount: number; } ``` ### `DismissAllResult` ```ts export interface DismissAllResult { /** How many notifications were read + dismissed by the sweep. */ dismissed: number; } ``` ### `DismissResult` ```ts export interface DismissResult { dismissed: true; } ``` ### `MysteryBoxReward` A resolved Mystery Box reward. The pool is coins-only today, so `type` is always `coins`; `is_lucky` is true when the draw landed in the rare tier. ```ts export interface MysteryBoxReward { type: 'coins'; amount: number; isLucky: boolean; } ``` ### `MysteryBoxState` The `GET /widget/mystery-box/state` payload. `locked: true` means the tenant's plan does not entitle the box — the widget renders nothing rather than treating it as an error. ```ts export interface MysteryBoxState { locked: boolean; eligible: boolean; /** Midnight UTC that opens the next claim, or null when eligible now. */ nextEligibleAt: string | null; /** The most recent reward this buddy drew, or null if never opened. */ lastReward: MysteryBoxReward | null; } ``` ### `MysteryBoxClaimResult` The `POST /widget/mystery-box/claim` payload. ```ts export interface MysteryBoxClaimResult { reward: MysteryBoxReward; claimedAt: string; } ``` ### `NarrativeProposalStatus` Lifecycle of a Council narrative proposal. ```ts export type NarrativeProposalStatus = | 'pending' | 'approved' | 'rejected' | 'live'; ``` ### `Proposal` One narrative proposal authored by a Council member. ```ts export interface Proposal { id: string; proposerBuddyId: string; /** The narrative slot the copy targets (e.g. `level_up_copy_stage_3`). */ targetSlot: string; proposedText: string; status: NarrativeProposalStatus; reviewerNote: string | null; reviewedAt: string | null; createdAt: string; } ``` ### `MyProposalsView` The `GET /widget/council/proposals/mine` payload. When the buddy is not a Council member (or the tenant is not entitled), `is_council_member` is false and the quota fields are zeroed — the widget renders its locked state. ```ts export interface MyProposalsView { isCouncilMember: boolean; /** Narrative stages open to Council proposals. */ allowedStages: number[]; /** Proposals the buddy may submit per ISO week. */ weeklyLimit: number; usedThisWeek: number; proposals: Proposal[]; } ``` ### `SubmitProposalParams` ```ts export interface SubmitProposalParams { /** The narrative slot key to target (e.g. `level_up_copy_stage_3`). */ targetSlot: string; proposedText: string; signal?: AbortSignal; } ``` ### `FreeLunchNotification` The `GET /widget/free-lunch/notification` payload. When there is nothing to show, `has_pending` is false and the reward fields are absent — the widget renders no banner. ```ts export interface FreeLunchNotification { hasPending: boolean; /** The grant to acknowledge once the banner is shown. */ grantId?: string; /** The coin amount the welcome credit granted. */ amount?: number; /** Tenant-configured banner copy. */ calloutCopy?: string; } ``` ### `FreeLunchAcknowledgeResult` ```ts export interface FreeLunchAcknowledgeResult { /** False when the grant was already acknowledged or did not match. */ acknowledged: boolean; } ``` ### `BeginnersLuckAwardedItem` The collectible a Beginner's Luck winner is awarded. ```ts export interface BeginnersLuckAwardedItem { id: string; name: string; thumbnailUrl: string | null; rarityLabel: string; } ``` ### `BeginnersLuckResult` The `GET /widget/beginners-luck/result` payload. The reveal is winner-only: losers and a disabled feature both resolve to `won: false` with no awarded item or copy. ```ts export interface BeginnersLuckResult { won: boolean; awardedItem: BeginnersLuckAwardedItem | null; calloutCopy: string | null; } ``` ### `RecordWidgetRenderedParams` ```ts export interface RecordWidgetRenderedParams { /** Widget mount IDs that rendered successfully on this page view. */ widgets?: string[]; /** Loader semantic version. */ loaderVersion?: string; /** Opaque loader build identifier. */ buildId?: string; signal?: AbortSignal; } ``` ### `WidgetRenderedResult` ```ts export interface WidgetRenderedResult { ok: true; /** True only for the first production render event accepted for the workspace. */ first: boolean; /** Present when the server accepts but intentionally ignores the beacon. */ skipped?: 'preview'; } ``` ### `KnownErrorCode` Union of every known error code string. Use this when annotating your own switch statement's exhaustiveness: ```ts function handle(code: KnownErrorCode) { switch (code) { ... } } ``` Unknown codes (e.g. ones added server-side between SDK releases) still arrive on `HatchedError.code` as plain strings; this union covers only what the current SDK knows about. ```ts export type KnownErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode]; ``` ### `paths` Auto-generated from apps/api/openapi.public.json. Do NOT edit by hand — run `pnpm --filter @hatched/sdk-js generate:types` after a contract change. The CI `check:types` step fails if this file drifts from the committed openapi.public.json. ```ts export type paths = { "/customers/me": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get: operations["CustomersController_getProfile"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch: operations["CustomersController_updateProfile"]; trace?: never; }; "/customers/me/referral": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get the current workspace referral link * @description Returns a referral link only after activation has reached first widget render. Referred signups grant promo AI credits to both workspaces. */ get: operations["CustomersController_getReferral"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/settings": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; delete?: never; options?: never; head?: never; patch: operations["CustomersController_updateSettings"]; trace?: never; }; "/customers/me/widget-theme/suggest": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Suggest widget theme customization * @description Drafts widget CSS variables and starter custom CSS from the customer visual theme. The dashboard applies it as an editable draft. */ post: operations["CustomersController_suggestWidgetTheme"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/audiences": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; delete?: never; options?: never; head?: never; /** * Replace the customer audience list * @description Defines 1..5 rol-based audiences. Removing an audience that still has buddies returns 409 audience_in_use. */ patch: operations["CustomersController_updateAudiences"]; trace?: never; }; "/customers/me/assets/regenerate": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Bulk regenerate AI assets * @description Flips every non-uploaded badge + item to pending and enqueues icon/image generation jobs. Use mode=missing to queue only failed/missing assets. Uploaded (manual) assets are skipped. */ post: operations["CustomersController_regenerateAssets"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/users/{user_id}/data": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; delete: operations["CustomersController_deleteUserData"]; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/feature-toggles": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get the tenant feature toggle state * @description Returns the effective toggle state (registry defaults merged with tenant overrides), the published map, and any pending draft. Drives the Gamification Planner. */ get: operations["CustomerFeatureToggleController_getState"]; put?: never; post?: never; delete?: never; options?: never; head?: never; /** * Update tenant feature toggles * @description Merges a partial toggle map into the tenant state. `mode: "draft"` keeps the change in a draft column; `mode: "publish"` writes through to the live `feature_toggles`. */ patch: operations["CustomerFeatureToggleController_updateToggles"]; trace?: never; }; "/customers/me/octalysis-state": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get the tenant Octalysis aggregate state * @description Returns effective toggles plus radar, hat balance, and brain balance aggregates. Cached for 60 seconds and invalidated by toggle writes. */ get: operations["CustomerFeatureToggleController_getOctalysisState"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/feature-toggles/publish": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Publish the pending draft toggle map * @description Promotes `feature_toggles_draft` to `feature_toggles` atomically and clears the draft. */ post: operations["CustomerFeatureToggleController_publishDraft"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/feature-toggles/draft": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; /** * Discard the pending draft toggle map * @description Clears `feature_toggles_draft` without affecting the published state. */ delete: operations["CustomerFeatureToggleController_discardDraft"]; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/feature-config": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get the tenant feature_config blob * @description Returns customer-scoped configuration for every Planner-managed feature, with zod defaults applied for keys the admin has not touched. */ get: operations["CustomerFeatureToggleController_getFeatureConfig"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/feature-config/{feature_key}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; delete?: never; options?: never; head?: never; /** * Update one feature_config block * @description Validates the body against the schema registered for `feature_key` and persists the parsed result. */ patch: operations["CustomerFeatureToggleController_updateFeatureConfig"]; trace?: never; }; "/customers/me/narrative": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get the tenant narrative state * @description Returns the effective (published, resolved over Hatched defaults) narrative, the raw published blob, and any pending draft. */ get: operations["CustomerNarrativeController_getNarrative"]; put?: never; post?: never; delete?: never; options?: never; head?: never; /** * Update the tenant narrative * @description Merges a partial narrative patch. `mode: "draft"` keeps the change in `narrative_draft`; `mode: "publish"` writes through to `narrative` and busts the widget caches. Every changed field is recorded in the narrative audit log. */ patch: operations["CustomerNarrativeController_updateNarrative"]; trace?: never; }; "/customers/me/narrative/audit": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * List narrative copy change history * @description Returns the most recent narrative field changes (B2B compliance). Filter by `field` (dot-notation path); `limit` defaults to 50, capped at 200. */ get: operations["CustomerNarrativeController_getNarrativeAudit"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/mission-anchor-config": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get the Mission Anchor admin config * @description Returns the resolved Mission Anchor policy — per-widget visibility, modal toggles, theme override, and the A/B test on the short copy. */ get: operations["CustomerMissionAnchorController_getConfig"]; put?: never; post?: never; delete?: never; options?: never; head?: never; /** * Update the Mission Anchor admin config * @description Merges a partial config patch. Each section present in the body (`visibility`, `modal`, `theme_override`, `ab_test`) replaces that section wholesale; missing sections are left untouched. Busts the widget cache. */ patch: operations["CustomerMissionAnchorController_updateConfig"]; trace?: never; }; "/customers/me/onboarding/six-d": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get the tenant 6D wizard state * @description Returns `null` when the tenant has not run the wizard yet; otherwise the full validated `six_d` config (objectives → fun + completed_at). */ get: operations["SixDWizardController_get"]; put?: never; /** * Apply the full 6D wizard payload * @description Writes through to `customers.feature_config.six_d`, optionally seeds the toggle draft with the wizard-recommended features, and records `six_d_wizard_applied` in the admin audit log. Completing the wizard also counts as onboarding: a first-time completion sets `preset_plan` and publishes a minimal config version, so login skips /onboarding/start and `POST /players/zero` works immediately. Re-applies never create extra config versions. */ post: operations["SixDWizardController_apply"]; delete?: never; options?: never; head?: never; /** * Patch one or more 6D wizard sections * @description Merges the partial payload over the current config, re-validates the full shape, and records `six_d_wizard_patched` in the admin audit log. Returns the new full config. */ patch: operations["SixDWizardController_patch"]; trace?: never; }; "/customers/me/onboarding/six-d/skip": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * HTCH-137 — Expert skip * @description Marks the wizard (at /onboarding/six-d) as completed with an empty objective set; the Planner then shows audience-only recommendations. Counts as onboarding the same way a full apply does (sets `preset_plan`, publishes a minimal config version). Records `six_d_wizard_skipped` in the audit log. */ post: operations["SixDWizardController_skip"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/onboarding/six-d/audit": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * HTCH-137 — Audit timeline * @description Returns the most recent `six_d_wizard_*` audit log entries for this tenant. Powers the wizard history timeline in Planner Insights. */ get: operations["SixDWizardController_audit"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/onboarding/six-d/drift-stats": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * HTCH-137 — Config drift stats * @description Returns the `six_d_wizard_applied` count over the last 30 days and a `drift_detected` flag (>= 3 reapplies). */ get: operations["SixDWizardController_driftStats"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/auth/register": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Register a new customer account */ post: operations["AuthController_register"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/auth/login": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Authenticate and obtain a JWT token */ post: operations["AuthController_login"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/auth/sso/config": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Return public dashboard SSO configuration */ get: operations["AuthController_getSsoConfig"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/auth/sso/start": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Start dashboard SSO via generic OIDC */ get: operations["AuthController_startSso"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/auth/sso/callback": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Complete dashboard SSO callback from OIDC provider */ get: operations["AuthController_completeSso"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/auth/password/change": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Change the current dashboard account password */ post: operations["AuthController_changePassword"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/auth/password/reset/request": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Request a dashboard password reset token */ post: operations["AuthController_requestPasswordReset"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/auth/password/reset": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Reset dashboard password with a one-time token */ post: operations["AuthController_resetPassword"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/auth/email/verify": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Verify dashboard account email with a one-time token */ post: operations["AuthController_verifyEmail"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/auth/email/verification/request": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Request a fresh dashboard email verification link */ post: operations["AuthController_requestEmailVerification"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/auth/me": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Get the currently authenticated customer profile */ get: operations["AuthController_me"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/auth/api-keys/whoami": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Return the identity, plan, capabilities, and scopes of the calling credential * @description Use this to validate an API key during onboarding/CI without performing a side-effectful call. For dashboard JWT auth, the api_key field is null. For secret API keys, scopes is an empty array (full surface). For publishable keys, scopes carries the publishable-scope list. */ get: operations["AuthController_whoami"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/auth/api-keys": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List all active API keys for the current customer */ get: operations["AuthController_listApiKeys"]; put?: never; /** Create a new API key */ post: operations["AuthController_createApiKey"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/auth/api-keys/rotate": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Rotate API keys by revoking all existing keys and creating a new one */ post: operations["AuthController_rotateApiKey"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/auth/api-keys/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; /** Revoke an API key by its ID */ delete: operations["AuthController_revokeApiKey"]; options?: never; head?: never; patch?: never; trace?: never; }; "/auth/publishable-keys": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Issue a browser-safe publishable key (hatch_pk_*) with a scoped set of permissions. */ post: operations["AuthController_createPublishableKey"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/operations/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Get an operation by its ID */ get: operations["OperationsController_findOne"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/operations": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List operations with optional type and status filters */ get: operations["OperationsController_findAll"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/operations/{id}/cancel": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Cancel a pending or processing operation */ post: operations["OperationsController_cancel"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget-sessions/preview": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Create automatic dashboard preview tokens * @description Finds the current customer’s latest active buddy and returns short-lived tokens for the widget customization preview. */ get: operations["WidgetSessionsController_createPreviewTokens"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/players/zero": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Read Player Zero status without provisioning it * @description Returns whether the workspace demo player exists and whether a hatch ceremony has completed for any buddy in the workspace. Read-only — never creates the player. Powers the activation checklist "Hatch your buddy" step. */ get: operations["WidgetSessionsController_getPlayerZeroStatus"]; put?: never; /** * Create or get the workspace demo player (Player Zero) * @description Idempotently provisions a canonical demo buddy ("Player Zero", user_id "player-0") for the current workspace, bound by every dashboard widget preview. Instant — uses a preset Fern sprite instead of the async AI image pipeline. It does not publish Planner drafts; preview tokens read drafts separately. */ post: operations["WidgetSessionsController_createPlayerZero"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/embed-tokens": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Create embed token * @description Generate a read-only embed token for widget rendering */ post: operations["WidgetSessionsController_createEmbedToken"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget-sessions": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Create session token * @description Create an interactive widget session with scoped permissions */ post: operations["WidgetSessionsController_createSessionToken"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget-sessions/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; /** * Revoke widget session * @description Revoke an active widget session by ID */ delete: operations["WidgetSessionsController_revokeSession"]; options?: never; head?: never; patch?: never; trace?: never; }; "/widget-sessions/verify-installation": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Verify widget installation * @description Server-side fetches a page and reports whether the Hatched loader snippet is present in the served HTML. Falls back to the first configured allowed origin when no URL is supplied. */ post: operations["WidgetSessionsController_verifyInstallation"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/buddy": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get widget buddy * @description Returns buddy canonical state along with widget configuration */ get: operations["WidgetApiController_getBuddy"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/buddy/share": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Mint (or fetch) the public share link for the widget buddy * @description Returns { enabled, share_code, share_url, card_url }. Mints the code on first call. Pass ?channel= to also log the share to that channel. */ post: operations["WidgetApiController_createShareLink"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/buddy/share/events": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Record a share-sheet funnel event (opened) */ post: operations["WidgetApiController_recordShareEvent"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/buddy/hatched": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Record that the hatch ceremony completed for the widget buddy * @description HTCH — persists `hatch_ceremony_seen` so read-only previews and re-mounts render the hatched buddy instead of replaying the egg ceremony. Requires an interactive session with events:track. Idempotent; the buddy widget calls it once the celebration beat settles. */ post: operations["WidgetApiController_markBuddyHatched"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/buddy/seo": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; delete?: never; options?: never; head?: never; /** * Set the per-buddy search-indexing preference * @description HTCH-66 — lets the end-user keep their public share page out of (or in) search engines. Omit `public_search_indexable` to inherit the tenant default. */ patch: operations["WidgetApiController_updateBuddySeo"]; trace?: never; }; "/widget/buddy/profile": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; delete?: never; options?: never; head?: never; /** * Update the buddy public Profile Page preferences * @description HTCH-101 — toggles, per buddy, whether the public Profile Page shows the "Champion of Season N" Hall of Fame trophies. */ patch: operations["WidgetApiController_updateBuddyProfile"]; trace?: never; }; "/widget/state": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get aggregate widget state * @description Single-request snapshot of everything a mounted widget set needs: buddy canonical state, badge catalog, and any async operations still pending for the buddy. Responds with an ETag — send it back via If-None-Match to get a 304 when nothing changed. */ get: operations["WidgetApiController_getState"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/narrative": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get the tenant narrative * @description Returns the published tenant narrative resolved over the Hatched defaults — the 5-act hatch ceremony copy, evolution level-up copy, win-state meaning templates, and mission fields. Cached server-side for 5 minutes. */ get: operations["WidgetApiController_getNarrative"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/narrative/arc": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get the Program Chapters arc * @description HTCH-15x — Yu-kai #10 Narrative. Returns the workspace program-chapter arc: authored chapters (empties dropped) plus each chapter’s reveal state, where a chapter is revealed once the workspace wins a group-quest tagged with its key. `available:false` → the arc is disabled or has no authored chapter and the widget renders nothing. Cached server-side for 5 minutes. */ get: operations["WidgetApiController_getNarrativeArc"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/mission-anchor": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get the Mission Anchor payload * @description HTCH-35 — the persistent CD1 mission badge for widget chrome. Returns the (A/B-resolved) short copy, the full mission, the user’s weekly contribution, and the admin anchor config. `anchor_config` is null when the mission_anchor feature is off — the widget then skips the mount. */ get: operations["WidgetApiController_getMissionAnchor"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/rendered": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Record a successful widget render * @description Loader-level activation beacon. Requires only a valid widget token, not events:track scope, and does not consume customer event quota. Preview tokens are ignored so dashboard previews do not claim production activation. */ post: operations["WidgetApiController_recordRendered"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/track": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Track event from browser * @description Browser-safe event ingestion. Derives user_id and audience from the widget token, so the browser only supplies type + optional properties. Returns the same effects payload as the server-to-server ingest so widgets can apply it optimistically. */ post: operations["WidgetApiController_trackEvent"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/marketplace": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get widget marketplace * @description Returns marketplace items visible to the buddy, with ownership and lock status */ get: operations["WidgetApiController_getMarketplace"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/marketplace/items/{id}/track-view": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Track marketplace item impression * @description Records an impression each time a widget surfaces an item. Feeds the marketplace funnel analytics. Fire-and-forget from the widget — never blocks the user flow. */ post: operations["WidgetApiController_trackMarketplaceItemView"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/purchase": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Purchase item * @description Purchase a marketplace item using coins. Requires marketplace:purchase scope. */ post: operations["WidgetApiController_purchaseItem"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/equip": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Equip or unequip items * @description Equip or unequip items on the buddy. Requires items:equip scope. */ post: operations["WidgetApiController_equipItems"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/appearance/rerender": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Rerender stage base * @description Regenerate the buddy’s bare stage image. Use when appearance.status is "failed" with code="needs_rerender" or after a hard generation failure that left the base contaminated. Equipped items are cleared from the rendered set; the user can re-equip after status returns to "ready". */ post: operations["WidgetApiController_rerenderAppearance"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/marketplace/preview-outfit": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Preview an outfit composition * @description Returns the cached composite variant for a slot→item map, or kicks off a background composition job and returns status="pending". The buddy state is NOT mutated. */ post: operations["WidgetApiController_previewOutfit"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/marketplace/composition-status": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Poll an outfit composition variant * @description Returns the current state of a buddy_image_variant row scoped to the current customer. Used by the dress-mode widget to swap the preview image once composition completes. */ get: operations["WidgetApiController_compositionStatus"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/marketplace/outfits": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * List saved outfits * @description Returns the buddy’s saved outfits ordered by active-first then most-recent. */ get: operations["WidgetApiController_listOutfits"]; put?: never; /** * Save a new outfit * @description Persists a slot→item map under a user-supplied name. Cap is driven by marketplace_dress.outfit_limit_per_buddy. */ post: operations["WidgetApiController_saveOutfit"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/marketplace/outfits/{id}/activate": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; delete?: never; options?: never; head?: never; /** * Activate a saved outfit * @description Marks the outfit active and delegates to the equip pipeline so the buddy’s composite stays in lockstep. */ patch: operations["WidgetApiController_activateOutfit"]; trace?: never; }; "/widget/marketplace/outfits/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; /** * Delete a saved outfit * @description Removes the outfit row for the current widget buddy. */ delete: operations["WidgetApiController_deleteOutfit"]; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/badges": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get widget badge catalog * @description Returns the buddy’s earned badges plus locked definitions with progress hints. HTCH-16: locked badges default-on so the collection grid surfaces "to go" tiles; pass `include_locked=false` to suppress them for tight embedded surfaces. */ get: operations["WidgetApiController_getBadges"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/path": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get the active path for the buddy’s audience * @description Resolves the audience’s currently active path definition and returns the buddy-scoped runtime payload (locked/available/completed sub-step states + current_step_key). Returns `null` when no path is active for this audience. */ get: operations["WidgetApiController_getActivePath"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/path/{key}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get a specific path for a buddy * @description Forces the runtime payload for a specific path key (caller-driven mount), independent of which path is currently active. */ get: operations["WidgetApiController_getPathByKey"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/path/{key}/sub-steps/{subKey}/complete": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Manually mark a sub-step complete * @description Idempotent on (buddy, sub-step). Only succeeds for sub-steps where allow_manual_complete=true. Returns the cascade flags so the widget can paint step.completed / path.completed celebrations without an extra fetch. */ post: operations["WidgetApiController_completePathSubStep"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/streak/{key}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get widget streak * @description Returns the current streak progress for a specific admin-defined streak key. The widget instance mounts with data-streak-key="..." to select which streak it renders. */ get: operations["WidgetApiController_getStreak"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/evolutions": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get widget evolution timeline * @description Returns the stage-transition history for the current buddy, newest first. Covers prod, auto-evolve, and demo paths. */ get: operations["WidgetApiController_getEvolutions"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/tokens": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get the buddy’s token wallet * @description Returns the spendable primary balance (coins) plus every active progression-token balance for the buddy’s audience. Used by the tokens widget to render a multi-currency wallet card. */ get: operations["WidgetApiController_getTokens"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/operations/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get widget operation * @description Get the status of an async operation for the current buddy */ get: operations["WidgetApiController_getOperation"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/theme": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Live widget theme * @description Returns the customer's authoritative widget theme (vars, custom CSS, size). Loader uses this to keep the deployed snippet in sync with Widget Studio without manual re-deploys. */ get: operations["WidgetApiController_getLiveTheme"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/next-best-action": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get the single next-best-action recommendation * @description HTCH-26 — Yu-kai #28 Desert Oasis. Runs eight strategies, applies tenant overrides (enable per kind, priority multiplier, copy override), and returns the winning action plus a `fallback_used` flag (true when the try_new_outfit strategy fired because nothing higher-priority matched). Cached per buddy for 30 seconds. */ get: operations["WidgetApiController_getNextBestAction"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/presets": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * List presets * @description List all available presets with summaries */ get: operations["PresetsController_listPresets"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/presets/{key}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get preset * @description Get full preset configuration by key */ get: operations["PresetsController_getPreset"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/apply-preset": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Apply preset * @description Apply a preset configuration to the authenticated customer */ post: operations["PresetsController_applyPreset"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/config-versions": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * List config versions * @description List all config versions for the customer */ get: operations["ConfigVersionsController_findAll"]; put?: never; /** * Create or open the draft config version * @description Idempotent for the "open a draft to edit" intent: with no (or an empty) snapshot it returns the existing unpublished draft if one exists, otherwise creates one. A non-empty snapshot is a deliberate create. This stops the dashboard "New draft" button (and any double-click) from 400-ing with "An unpublished draft already exists". */ post: operations["ConfigVersionsController_create"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/config-versions/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get config version * @description Get a single config version by ID */ get: operations["ConfigVersionsController_findById"]; put?: never; post?: never; delete?: never; options?: never; head?: never; /** * Update config version * @description Update a draft config version */ patch: operations["ConfigVersionsController_update"]; trace?: never; }; "/config-versions/{id}/publish": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Publish config version * @description Publish a draft config version, making it the active configuration */ post: operations["ConfigVersionsController_publish"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/config-versions/{id}/impact": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Preview config impact * @description Get an impact preview for a draft config version before publishing */ get: operations["ConfigVersionsController_getImpact"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/config-versions/{id}/clone": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Clone config version * @description Create a new draft from an existing config version */ post: operations["ConfigVersionsController_clone"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/config-versions/{id}/migrate-buddies": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Migrate buddies * @description Re-point active buddies to a published config version */ post: operations["ConfigVersionsController_migrateBuddies"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/eggs": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List eggs with optional user and status filters */ get: operations["EggsController_findAll"]; put?: never; /** Create a new egg for a user */ post: operations["EggsController_create"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/eggs/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Get an egg by its ID */ get: operations["EggsController_findById"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/eggs/{id}/status": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; delete?: never; options?: never; head?: never; /** Update an egg status to ready or cancelled */ patch: operations["EggsController_updateStatus"]; trace?: never; }; "/eggs/{id}/hatch": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Start the asynchronous hatch process for an egg */ post: operations["EggsController_hatch"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/buddies": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List buddies with pagination and optional filters */ get: operations["BuddiesController_findAll"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/buddies/users/{user_id}/summary": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Get a user summary including buddy count, coins, and badges */ get: operations["BuddiesController_getUserSummary"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/buddies/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Get buddy details with full canonical state */ get: operations["BuddiesController_findById"]; put?: never; post?: never; delete?: never; options?: never; head?: never; /** Update a buddy name */ patch: operations["BuddiesController_update"]; trace?: never; }; "/buddies/{id}/archive": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; delete?: never; options?: never; head?: never; /** Archive a buddy (one-way transition from active to archived) */ patch: operations["BuddiesController_archive"]; trace?: never; }; "/buddies/{id}/skills": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; delete?: never; options?: never; head?: never; /** Update buddy skill levels (increase, decrease, or set) */ patch: operations["BuddiesController_updateSkills"]; trace?: never; }; "/buddies/{buddy_id}/coins": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Earn coins for a buddy (supports idempotency) */ post: operations["BuddiesController_earnCoins"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/buddies/{buddy_id}/coins/spend": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Spend coins for a buddy (supports idempotency) */ post: operations["BuddiesController_spendCoins"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/buddies/{buddy_id}/badges": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List all badges awarded to a buddy */ get: operations["BuddiesController_getBuddyBadges"]; put?: never; /** Award a badge to a buddy */ post: operations["BuddiesController_awardBadge"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/buddies/{buddy_id}/evolution": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Check evolution readiness and progress for a buddy */ get: operations["BuddiesController_getEvolutionReadiness"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/buddies/{buddy_id}/evolve": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Trigger asynchronous buddy evolution */ post: operations["BuddiesController_evolve"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/buddies/{buddy_id}/purchase-item": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Purchase a marketplace item using coins (supports idempotency) */ post: operations["BuddiesController_purchaseItem"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/buddies/{buddy_id}/unlock-item": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Unlock an item without spending coins */ post: operations["BuddiesController_unlockItem"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/buddies/{buddy_id}/equipped-items": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; delete?: never; options?: never; head?: never; /** Equip or unequip items on a buddy */ patch: operations["BuddiesController_equipItems"]; trace?: never; }; "/buddies/{buddy_id}/appearance/rerender": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Regenerate the buddy stage base image */ post: operations["BuddiesController_rerenderAppearance"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/buddies/{buddy_id}/purchased-items": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List all purchased items for a buddy */ get: operations["BuddiesController_getPurchasedItems"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/buddies/{buddy_id}/tokens": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Typed token balances (primary + progression) * @description Returns the buddy's balances grouped by token kind. Each slot is null if the customer has not configured that kind. */ get: operations["BuddiesController_getTokens"]; put?: never; /** Earn or spend tokens for a buddy (supports idempotency) */ post: operations["BuddiesController_tokenTransaction"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/buddies/{buddy_id}/evolutions": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Stage transition timeline for a buddy */ get: operations["BuddiesController_listEvolutions"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/buddies/{buddy_id}/progression": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Get buddy progression metrics (legacy endpoint) */ get: operations["BuddiesController_getProgressionLegacy"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/buddies/{buddy_id}/progression-metrics": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Get buddy progression metrics (lessons, quizzes, streaks, etc.) */ get: operations["BuddiesController_getProgression"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/users/{user_id}/summary": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get: operations["CustomersSummaryController_getUserSummary"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/buddies/{id}/share": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Mint (or fetch) the public share link for a buddy */ post: operations["BuddyShareController_createShare"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/buddies/{id}/share/events": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Record a share-sheet funnel event (opened / shared) */ post: operations["BuddyShareController_recordEvent"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/buddy-share/stats": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Rolling-window buddy-share funnel for the tenant */ get: operations["BuddyShareStatsController_getStats"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/buddy-share/settings": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Tenant buddy-share settings (toggles + resolved link origin) */ get: operations["BuddyShareStatsController_getSettings"]; put?: never; post?: never; delete?: never; options?: never; head?: never; /** Update buddy-share toggles (enabled / show_tenant_name / cta_url) */ patch: operations["BuddyShareStatsController_updateSettings"]; trace?: never; }; "/public/b/{code}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Resolve a buddy share code to its public card data */ get: operations["PublicShareController_getPublicBuddy"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/public/b/{code}/events": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Record a share-page funnel event (viewed / cta_clicked) */ post: operations["PublicShareController_recordEvent"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/public/share-index": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List public share codes eligible for the sitemap (paged) */ get: operations["PublicShareIndexController_getShareIndex"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/p/{code}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Resolve a share code to the rich Profile Page v1 payload */ get: operations["ProfilePageController_getProfile"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/skill-sets": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * List all skill sets * @description Returns all skill sets configured for the current customer. */ get: operations["SkillSetsController_findAll"]; put?: never; /** * Create a skill set * @description Create a new skill set with one or more skill definitions. */ post: operations["SkillSetsController_create"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/skill-sets/generate-icon": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Generate a skill icon with AI * @description Runs the AI image pipeline to create a skill icon for the given label/description. Returns a stored icon URL, the generation prompt, and the recorded cost. */ post: operations["SkillSetsController_generateIcon"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/skill-sets/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get a skill set * @description Returns a single skill set by its ID, including all skill definitions. */ get: operations["SkillSetsController_findById"]; /** * Update a skill set * @description Update an existing skill set and its skill definitions by ID. */ put: operations["SkillSetsController_update"]; post?: never; /** * Delete a skill set * @description Permanently delete a skill set. Fails if any buddies are currently using it. */ delete: operations["SkillSetsController_delete"]; options?: never; head?: never; patch?: never; trace?: never; }; "/skill-rules": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * List all skill rules * @description Returns all skill rules configured for the current customer. */ get: operations["SkillRulesController_findAll"]; put?: never; /** * Create a skill rule * @description Create a new skill rule that defines how skill XP is awarded for a specific trigger event. */ post: operations["SkillRulesController_create"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/skill-rules/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; /** * Update a skill rule * @description Update an existing skill rule by ID. */ put: operations["SkillRulesController_update"]; post?: never; /** * Delete a skill rule * @description Permanently delete a skill rule by ID. */ delete: operations["SkillRulesController_delete"]; options?: never; head?: never; patch?: never; trace?: never; }; "/skill-rules/apply-theme-pack": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Apply a theme-aware skill-rule pack (HTCH-128) * @description Appends a preset bundle of 3–6 skill rules (industry × theme_vibe) onto the tenant draft config. Duplicates are skipped (never overwritten). The response carries created + skipped + a theme/industry mismatch warning when applicable. */ post: operations["SkillRulesController_applyThemePack"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/skill-decay-rules": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List skill decay rules */ get: operations["SkillDecayRulesController_findAll"]; put?: never; /** * Create a skill decay rule * @description Define how a skill level decays over time (daily/weekly/monthly). Decay is opt-in per customer via settings.features.decay. */ post: operations["SkillDecayRulesController_create"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/skill-decay-rules/{id}/preview": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Preview the cumulative effect of a decay rule * @description Returns the projected skill level after N cadence periods given a starting level. Used by the dashboard to show "in 30 days, 100 → ?". */ get: operations["SkillDecayRulesController_preview"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/skill-decay-rules/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; /** Update a skill decay rule */ put: operations["SkillDecayRulesController_update"]; post?: never; /** Delete a skill decay rule */ delete: operations["SkillDecayRulesController_delete"]; options?: never; head?: never; patch?: never; trace?: never; }; "/skill-decay-rules/{id}/history": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Recent decay applications for a rule * @description Returns the latest applications recorded against this rule so the dashboard can show "last 30 days" activity per rule. */ get: operations["SkillDecayRulesController_history"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/skill-decay-rules/run-now": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Trigger a decay sweep immediately for this customer * @description Enqueues a one-off decay sweep scoped to this customer. The sweep is idempotent (a buddy already decayed for the current cadence period will not be touched), so this is safe to run during the day for QA or when an operator activates a rule and wants to see the effect now instead of waiting for the 03:00 UTC cron. */ post: operations["SkillDecayRulesController_runNow"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/coin-rules": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * List all coin rules * @description Returns all coin rules configured for the current customer. */ get: operations["CoinRulesController_findAll"]; put?: never; /** * Create a coin rule * @description Create a new coin rule that defines how coins are awarded for a specific trigger event. */ post: operations["CoinRulesController_create"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/coin-rules/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; /** * Update a coin rule * @description Update an existing coin rule by ID. */ put: operations["CoinRulesController_update"]; post?: never; /** * Delete a coin rule * @description Permanently delete a coin rule by ID. */ delete: operations["CoinRulesController_delete"]; options?: never; head?: never; patch?: never; trace?: never; }; "/coin-rules/{id}/reward-pool/telemetry": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Reward pool telemetry * @description Returns the real reward-pool pick distribution for a coin rule over the last 30 days, for the Planner drawer believability check. */ get: operations["CoinRulesController_rewardPoolTelemetry"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/economy/buddies/{buddyId}/ledger": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get coin ledger for a buddy * @description Returns a paginated list of coin transactions (credits and debits) for a specific buddy. */ get: operations["EconomyController_getLedger"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/badge-definitions": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * List all badge definitions * @description Returns all badge definitions configured for the current customer. */ get: operations["BadgeDefinitionsController_findAll"]; put?: never; /** * Create a badge definition * @description Create a new badge definition with criteria for automatic or manual awarding. */ post: operations["BadgeDefinitionsController_create"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/badge-definitions/upload-icon": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Upload a badge icon * @description Upload a PNG or SVG image file to use as a badge icon. Max size: 500KB. */ post: operations["BadgeDefinitionsController_uploadIcon"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/badge-definitions/generate-icon": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Generate a badge icon with AI * @description Runs the AI image pipeline to create a badge icon for the given label/description. Returns a stored icon URL, the generation prompt, and the recorded cost. */ post: operations["BadgeDefinitionsController_generateIcon"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/badge-definitions/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get a badge definition * @description Returns a single badge definition by its ID. */ get: operations["BadgeDefinitionsController_findById"]; /** * Update a badge definition * @description Update an existing badge definition by ID. */ put: operations["BadgeDefinitionsController_update"]; post?: never; /** * Delete a badge definition * @description Permanently delete a badge definition. Fails if awards already exist for this badge. */ delete: operations["BadgeDefinitionsController_delete"]; options?: never; head?: never; patch?: never; trace?: never; }; "/badge-definitions/{id}/regenerate-icon": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Queue an AI regeneration for this badge icon * @description Flips the badge to icon_status=pending and enqueues a background job that drafts a fresh prompt from the customer theme and re-renders the medallion. */ post: operations["BadgeDefinitionsController_regenerateIcon"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/event-badges": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List event-triggered badge campaigns */ get: operations["EventTriggeredBadgeController_list"]; put?: never; /** Create an event-triggered badge campaign */ post: operations["EventTriggeredBadgeController_create"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/event-badges/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; /** Delete an event-triggered badge campaign */ delete: operations["EventTriggeredBadgeController_remove"]; options?: never; head?: never; /** Update an event-triggered badge campaign */ patch: operations["EventTriggeredBadgeController_update"]; trace?: never; }; "/streak-definitions": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List all streak definitions */ get: operations["StreakDefinitionsController_findAll"]; put?: never; /** Create a streak definition */ post: operations["StreakDefinitionsController_create"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/streak-definitions/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Get a streak definition */ get: operations["StreakDefinitionsController_findById"]; /** Update a streak definition */ put: operations["StreakDefinitionsController_update"]; post?: never; /** Delete a streak definition */ delete: operations["StreakDefinitionsController_delete"]; options?: never; head?: never; patch?: never; trace?: never; }; "/image-usage": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get image usage * @description Get current image generation usage and limits for the customer */ get: operations["ImageCostController_getUsage"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/image-usage/report": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get image usage report * @description Get a detailed image generation usage report for a specific month */ get: operations["ImageCostController_getUsageReport"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/events": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * List events * @description List events with optional filters for user, type, date range, and limit */ get: operations["EventsController_findAll"]; put?: never; /** * Ingest event * @description Ingest a new event from an integration. Triggers coin, badge, and skill rules. */ post: operations["EventsController_ingest"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/events/admin-trigger": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Trigger an event from the dashboard admin tools * @description Runs the same event ingestion and rule-engine path as the public event API, but accepts dashboard JWT auth for operator QA and support actions. */ post: operations["EventsController_adminTrigger"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/events/batch": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Ingest event batch * @description Ingest multiple events in a single request (max 100) */ post: operations["EventsController_ingestBatch"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/events/types": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * List distinct event types * @description Returns distinct event type strings ever ingested for this customer. Used by rule editors so operators pick from observed events instead of a hardcoded list. */ get: operations["EventsController_findTypes"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/events/active-users": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * List most-active users in a recent window * @description Returns a leaderboard of users (players) ordered by event volume in the last N hours. Each row carries event count, last activity, top event types, and active-buddy count — enough for a single-glance activity feed. */ get: operations["EventsController_findActiveUsers"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/events/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get event * @description Get a single event by ID including its processing effects */ get: operations["EventsController_findById"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/event-types": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List event types */ get: operations["EventTypesController_findAll"]; put?: never; /** Register an event type */ post: operations["EventTypesController_create"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/event-types/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Get an event type */ get: operations["EventTypesController_findById"]; /** * Update or rename an event type * @description Renaming propagates to coin_rules, skill_rules, badge_definitions, streak_definitions, webhook_configs, event_ingestions, and custom_counters keys — all in a single transaction. */ put: operations["EventTypesController_update"]; post?: never; /** * Delete an event type * @description Rejects the delete if any rule references this event. 409 response includes per-table reference counts. */ delete: operations["EventTypesController_delete"]; options?: never; head?: never; patch?: never; trace?: never; }; "/token-config": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * List token configurations * @description Returns all token type configurations for the current customer. */ get: operations["TokenConfigController_list"]; put?: never; /** * Upsert token configuration * @description Create or update the token type configurations for the current customer. */ post: operations["TokenConfigController_upsert"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/marketplaces": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * List marketplaces * @description List all marketplaces for the authenticated customer */ get: operations["MarketplaceController_list"]; put?: never; /** * Create marketplace * @description Create a new marketplace for the authenticated customer */ post: operations["MarketplaceController_create"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/marketplaces/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get marketplace * @description Get a single marketplace by ID */ get: operations["MarketplaceController_findOne"]; /** * Update marketplace * @description Update an existing marketplace */ put: operations["MarketplaceController_update"]; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/marketplaces/{id}/items": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * List items * @description List items in a marketplace with optional filters and pagination */ get: operations["MarketplaceController_listItems"]; put?: never; /** * Create item * @description Create a new item in a marketplace */ post: operations["MarketplaceController_createItem"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/marketplaces/{id}/items/import": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Import items * @description Bulk import items into a marketplace from JSON or CSV */ post: operations["MarketplaceController_importItems"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/marketplaces/{id}/items/{item_id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get item * @description Get a single item by ID within a marketplace */ get: operations["MarketplaceController_findItem"]; /** * Update item * @description Update an existing marketplace item */ put: operations["MarketplaceController_updateItem"]; post?: never; /** * Delete item * @description Delete a marketplace item */ delete: operations["MarketplaceController_deleteItem"]; options?: never; head?: never; patch?: never; trace?: never; }; "/marketplaces/{id}/items/reorder": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Reorder items * @description Set the display order of items in a marketplace */ post: operations["MarketplaceController_reorderItems"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/marketplaces/{id}/items/{item_id}/upload-image": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Upload item image * @description Upload a PNG image for a marketplace item (max 2MB) */ post: operations["MarketplaceController_uploadItemImage"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/marketplaces/{id}/items/{item_id}/regenerate-image": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Queue an AI regeneration for this item image * @description Flips the item to image_status=pending and enqueues a background job that drafts a fresh prompt from the customer theme and re-renders the item art. */ post: operations["MarketplaceController_regenerateItemImage"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/items/{id}/gift": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Gift a marketplace item to a teammate * @description Debits the sender and writes the item into the recipient inventory with gift metadata. 402 when the sender is short on coins; 200 with duplicate=true when the same gift is re-sent inside the 60s window. */ post: operations["MarketplaceWidgetController_gift"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/flash-sales": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List flash sales for the Planner drawer */ get: operations["FlashSaleController_list"]; put?: never; /** Schedule a flash sale */ post: operations["FlashSaleController_schedule"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/flash-sales/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; /** Cancel a flash sale — a running sale clears its discounts */ delete: operations["FlashSaleController_cancel"]; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/marketplace/fomo": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Marketplace FOMO poll * @description Records the buddy as a current viewer of every supplied item and returns per-item viewer counts, real stock and flash-sale discounted prices, plus the active flash sale. Returns enabled=false when the tenant has the marketplace_fomo feature switched off. */ post: operations["MarketplaceFomoWidgetController_resolve"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/buddy/returning-champion": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get the Returning Champion re-onboarding scene payload * @description HTCH-100 — resolves whether this buddy is a 30+ day-lapsed past LEAGUES champion. Returns `{ available: false }` for everyone else. Clears an expired legacy-crown overlay as a side effect. */ get: operations["LegacyEquipController_getReturningChampion"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/buddy/returning-champion/dismiss": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Dismiss the Returning Champion scene * @description HTCH-100 — skip or continue past the re-onboarding scene. Clears the payload and resumes the HTCH-91 lapsed sweep; the 90-day cooldown stands so the scene cannot re-trigger before then. */ post: operations["LegacyEquipController_dismissReturningChampion"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/buddy/equip-legacy-item": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Temp-equip a legacy crown for the returning-champion scene * @description HTCH-100 — re-equips a still-owned legendary item as a 7-day temporary overlay (Endowment Effect). It never mutates the buddy’s real inventory and triggers no image re-composition. */ post: operations["LegacyEquipController_equipLegacyItem"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/public/returning-champion/welcome-back": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Resolve a Returning Champion welcome-back token to a widget session */ get: operations["ReturningChampionPublicController_resolveWelcomeBack"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/webhook-configs": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * List webhook configs * @description List all webhook configurations for the customer */ get: operations["WebhooksController_list"]; put?: never; /** * Create webhook config * @description Register a new webhook endpoint to receive event notifications */ post: operations["WebhooksController_create"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/webhook-configs/events": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * List webhook event types * @description Return the canonical event types accepted by webhook subscriptions. */ get: operations["WebhooksController_listEvents"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/webhook-configs/health": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get webhook delivery health * @description Customer-scoped delivery health, active endpoint count, alerts, and digest status. */ get: operations["WebhooksController_getHealth"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/webhook-configs/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get webhook config * @description Get a single webhook configuration by ID */ get: operations["WebhooksController_findOne"]; /** * Update webhook config * @description Update an existing webhook configuration */ put: operations["WebhooksController_update"]; post?: never; /** * Delete webhook config * @description Delete a webhook configuration */ delete: operations["WebhooksController_delete"]; options?: never; head?: never; patch?: never; trace?: never; }; "/webhook-configs/{id}/rotate-secret": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Rotate webhook secret * @description Generate a new signing secret for a webhook endpoint */ post: operations["WebhooksController_rotateSecret"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/webhook-configs/{id}/test": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Send test webhook * @description Send a test payload to the webhook endpoint */ post: operations["WebhooksController_sendTest"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/webhook-configs/{id}/deliveries": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * List webhook deliveries * @description Get delivery log history for a webhook config */ get: operations["WebhooksController_getDeliveries"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/webhook-configs/{id}/deliveries/{deliveryId}/redeliver": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Redeliver webhook * @description Re-enqueue a previous delivery using its stored payload. */ post: operations["WebhooksController_redeliver"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/buddy/prestige": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Whether the current buddy can prestige, and why not if it cannot */ get: operations["PrestigeController_status"]; put?: never; /** Prestige the current buddy — reset to stage 0 for a prestige level */ post: operations["PrestigeController_prestige"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/analytics/overview": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get analytics overview * @description Get high-level platform analytics overview for the customer */ get: operations["AnalyticsController_getOverview"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/analytics/engagement": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get engagement metrics * @description Get user engagement metrics over time with configurable period and date range */ get: operations["AnalyticsController_getEngagement"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/analytics/activity-summary": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get activity summary * @description Get aggregated activity summary for the customer */ get: operations["AnalyticsController_getActivitySummary"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/analytics/economy-summary": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get economy summary * @description Get coin economy summary including earnings and spending */ get: operations["AnalyticsController_getEconomySummary"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/analytics/economy-health": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get economy health * @description Get health indicators for the coin economy (inflation, velocity, etc.) */ get: operations["AnalyticsController_getEconomyHealth"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/analytics/popular-items": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get popular items * @description Get the most popular marketplace items by purchase count */ get: operations["AnalyticsController_getPopularItems"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/analytics/popular-badges": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get popular badges * @description Get the most frequently awarded badges */ get: operations["AnalyticsController_getPopularBadges"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/analytics/retention": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get retention metrics * @description Get cohort retention analysis for user engagement */ get: operations["AnalyticsController_getRetention"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/analytics/roi-metrics": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get ROI metrics * @description Get return on investment metrics for gamification features */ get: operations["AnalyticsController_getRoiMetrics"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/analytics/streaks": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get streak health * @description Active streak rate, milestone hit rates, and length distribution */ get: operations["AnalyticsController_getStreakHealth"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/analytics/tokens": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get per-token economy * @description Earn, spend, net, and holder counts per token key */ get: operations["AnalyticsController_getTokenEconomy"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/analytics/audiences": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get audience breakdown * @description Per-audience DAU, events, coins earned, avg streak */ get: operations["AnalyticsController_getAudienceBreakdown"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/analytics/evolution": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get evolution timeline * @description Per-stage buddy counts and time-to-reach statistics */ get: operations["AnalyticsController_getEvolutionTimeline"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/analytics/marketplace-funnel": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get marketplace funnel * @description Per-item views / purchases / equips conversion funnel */ get: operations["AnalyticsController_getMarketplaceFunnel"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/analytics/webhooks": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get webhook delivery health * @description Success rate, latency percentiles, retries, failures by event */ get: operations["AnalyticsController_getWebhookHealth"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/analytics/event-counts": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Per-event counts * @description Per-event aggregates (24h / 7d / 30d + last_seen) for every event ingested by the customer. Powers the /dashboard/events Lexicon page. */ get: operations["AnalyticsController_getEventCounts"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/analytics/custom-events": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get custom event trends * @description Top event types with daily trend sparklines */ get: operations["AnalyticsController_getCustomEventTrends"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/analytics/feature-activity": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get feature activity * @description Weekly usage counts per Planner feature (kudos, group quests, mystery box, free lunch, outfits), rolled up from system telemetry events. */ get: operations["AnalyticsController_getFeatureActivity"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/leaderboard": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get leaderboard * @description HTCH-18: returns the leaderboard in one of three Yu-kai Ch.6 view modes — top, around_me, hybrid (default driven by tenant LeaderboardConfig). User-side query param overrides tenant default for the requesting widget. */ get: operations["LeaderboardController_getLeaderboard"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/leaderboard-config": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get the tenant leaderboard view-mode config * @description Returns the effective LeaderboardConfig (defaults applied) used by the widget. No separate draft slot: the drawer holds unsaved state client-side, persist = publish. */ get: operations["LeaderboardConfigController_get"]; put?: never; post?: never; delete?: never; options?: never; head?: never; /** * Update the tenant leaderboard view-mode config * @description Validates the body against the shared zod schema, upserts the dedicated row, and busts the `leaderboard:config:{customer_id}` cache so the next widget read sees the new view mode immediately. */ patch: operations["LeaderboardConfigController_patch"]; trace?: never; }; "/customers/me/analytics/share-funnel": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Public share-page gift funnel (gift CTA + signup wall) */ get: operations["ShareFunnelController_giftFunnel"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/billing/status": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get billing status * @description Get the current billing status, plan, credit balances, event usage, and live subscription summary for the customer. */ get: operations["BillingController_getStatus"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/billing/checkout": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Create checkout session * @description Create a Stripe checkout session for subscription (flow=subscription) or top-up (flow=credit_bundle) */ post: operations["BillingController_createCheckout"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/billing/checkout/session/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Reconcile a Stripe checkout session * @description Synchronously confirm a completed Stripe Checkout Session on the dashboard return (?session_id=...) and idempotently apply the plan/credits if the async webhook has not yet. Returns the reconciled billing state so the UI can show an honest status before bridging into onboarding. Safe to call repeatedly. */ get: operations["BillingController_reconcileCheckout"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/billing/portal": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Create billing portal session * @description Create a Stripe billing portal session for subscription management. */ post: operations["BillingController_createPortal"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/credits/balance": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get credit balance * @description Returns welcome, paid and promo pool balances plus total spendable. */ get: operations["CreditsController_getBalance"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/credits/ledger": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * List recent AI usage ledger entries * @description Paginated ledger of AI jobs (authorize / commit / rollback). */ get: operations["CreditsController_listLedger"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/onboarding/sessions": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Create or resume the current onboarding session */ post: operations["OnboardingController_create"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/onboarding/sessions/preparing-status": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Aggregate asset-generation status for the current customer */ get: operations["OnboardingController_preparingStatus"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/onboarding/sessions/seed-from-url": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Seed onboarding from a landing-page URL */ post: operations["OnboardingController_seedFromUrl"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/onboarding/sessions/seed-from-repo": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Seed onboarding from a repo-analysis brief produced by a local AI agent */ post: operations["OnboardingController_seedFromRepo"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/onboarding/sessions/seed-from-description": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Seed onboarding from operator-provided chips + optional description */ post: operations["OnboardingController_seedFromDescription"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/onboarding/sessions/waitlist": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Join the waitlist for an upcoming onboarding channel */ post: operations["OnboardingController_joinWaitlist"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/onboarding/sessions/current": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Fetch the current onboarding session */ get: operations["OnboardingController_current"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/onboarding/sessions/reset": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Reset the current onboarding session */ post: operations["OnboardingController_reset"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/onboarding/sessions/{id}/answers": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; /** Patch structured onboarding answers */ put: operations["OnboardingController_updateAnswers"]; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/onboarding/sessions/{id}/message": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Send a user message and stream the assistant reply via server-sent events */ post: operations["OnboardingController_message"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/onboarding/sessions/{id}/generate-plan": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Generate a gamification plan from the conversation */ post: operations["OnboardingController_generatePlan"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/onboarding/sessions/{id}/regenerate-plan": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Regenerate the plan with a variant seed */ post: operations["OnboardingController_regeneratePlan"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/onboarding/sessions/{id}/apply": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Apply the generated plan to the customer (writes gamification config) */ post: operations["OnboardingController_apply"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/onboarding/sessions/{id}/generate-guide": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Generate a personalized integration guide */ post: operations["OnboardingController_generateGuide"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/stage-assets": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List per-customer stage assets (preset mode buddy art) plus the default library URLs resolved for the customer's creature_style. */ get: operations["StageAssetsController_list"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/stage-assets/upload-url": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Issue a presigned PUT URL for a client-side stage asset upload */ post: operations["StageAssetsController_createUploadUrl"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/stage-assets/{stage}/regenerate": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Queue AI generation for a customer preset stage asset */ post: operations["StageAssetsController_regenerate"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/stage-assets/{stage}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; /** Commit an uploaded object as the preset asset for a stage */ put: operations["StageAssetsController_commit"]; post?: never; /** Remove the preset asset for a stage */ delete: operations["StageAssetsController_remove"]; options?: never; head?: never; patch?: never; trace?: never; }; "/gates": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List token gates for this customer */ get: operations["GatesController_list"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/gates/{gate_key}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; /** Create or update a token gate */ put: operations["GatesController_upsert"]; post?: never; /** Delete a token gate */ delete: operations["GatesController_remove"]; options?: never; head?: never; patch?: never; trace?: never; }; "/buddies/{buddy_id}/gates/{gate_key}/unlock": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Spend tokens to unlock a gate for a buddy * @description Idempotent. Returns already_unlocked: true on repeat calls without touching the economy. */ post: operations["GatesController_unlock"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/buddies/{buddy_id}/unlocks": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List gates this buddy has unlocked */ get: operations["GatesController_listUnlocks"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/path-definitions": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List path definitions */ get: operations["PathsController_listDefinitions"]; put?: never; /** Create a path definition */ post: operations["PathsController_createDefinition"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/path-definitions/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Get a path definition (with steps + sub-steps) */ get: operations["PathsController_getDefinition"]; /** Update a path definition */ put: operations["PathsController_updateDefinition"]; post?: never; /** Delete a path definition */ delete: operations["PathsController_deleteDefinition"]; options?: never; head?: never; patch?: never; trace?: never; }; "/path-definitions/{id}/activate": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Activate a path (atomic single-active per audience) * @description Sets is_active=true on this path and is_active=false on every other path with the same (customer, audience) in a single transaction. */ post: operations["PathsController_activate"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/path-definitions/{id}/deactivate": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Deactivate a path */ post: operations["PathsController_deactivate"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/path-definitions/{id}/steps": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List steps in a path */ get: operations["PathsController_listSteps"]; put?: never; /** Create a step in a path */ post: operations["PathsController_createStep"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/path-definitions/{id}/steps/reorder": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; /** Reorder steps in a path */ put: operations["PathsController_reorderSteps"]; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/path-definitions/{id}/steps/{stepId}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; /** Update a step */ put: operations["PathsController_updateStep"]; post?: never; /** Delete a step */ delete: operations["PathsController_deleteStep"]; options?: never; head?: never; patch?: never; trace?: never; }; "/path-definitions/{id}/steps/{stepId}/sub-steps": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List sub-steps within a step */ get: operations["PathsController_listSubSteps"]; put?: never; /** Create a sub-step */ post: operations["PathsController_createSubStep"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/path-definitions/{id}/steps/{stepId}/sub-steps/reorder": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; /** Reorder sub-steps within a step */ put: operations["PathsController_reorderSubSteps"]; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/path-definitions/{id}/steps/{stepId}/sub-steps/{subStepId}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; /** Update a sub-step */ put: operations["PathsController_updateSubStep"]; post?: never; /** Delete a sub-step */ delete: operations["PathsController_deleteSubStep"]; options?: never; head?: never; patch?: never; trace?: never; }; "/path-definitions/buddies/{buddyId}/paths/{pathKey}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get path runtime payload for a buddy * @description Returns the buddy-scoped path with sub-step status (locked/available/completed). Admin/api-key path; the widget uses /widget/path instead. */ get: operations["PathsController_getBuddyPath"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/path-definitions/buddies/{buddyId}/paths/{pathKey}/sub-steps/{subKey}/complete": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Manually mark a sub-step complete (admin) */ post: operations["PathsController_manualComplete"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/beginners-luck/analytics": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Beginner's Luck winner analytics * @description Returns the per-tenant evaluation totals — total evaluations, total winners, winner percentage, last-30-day winners, and the configured expected rate. */ get: operations["BeginnersLuckController_getAnalytics"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/beginners-luck/result": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get the buddy's Beginner's Luck result * @description HTCH-43 — idempotently evaluates Beginner's Luck for the first hatch and returns the winner-only reveal payload. Losers and a disabled feature both resolve to `won: false` with no awarded item or copy. */ get: operations["BeginnersLuckWidgetController_getResult"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/free-lunch/notification": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get the buddy's pending Free Lunch banner * @description HTCH-44 — returns the buddy's most recent unacknowledged Free Lunch grant, or `has_pending: false` when there is nothing to show. */ get: operations["FreeLunchWidgetController_getNotification"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/free-lunch/{id}/acknowledge": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Acknowledge a Free Lunch banner * @description HTCH-44 — marks the grant dismissed so the banner does not reappear. */ post: operations["FreeLunchWidgetController_acknowledge"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/teams": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List the tenant teams with member counts */ get: operations["TeamsAdminController_list"]; put?: never; /** Create a team */ post: operations["TeamsAdminController_create"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/teams/{teamId}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; /** Soft-delete a team and archive its memberships */ delete: operations["TeamsAdminController_remove"]; options?: never; head?: never; /** Update a team */ patch: operations["TeamsAdminController_update"]; trace?: never; }; "/customers/me/teams/{teamId}/members": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List the active members of a team */ get: operations["TeamsAdminController_listMembers"]; put?: never; /** Add a buddy to a team */ post: operations["TeamsAdminController_addMember"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/teams/{teamId}/members/{buddyId}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; /** Remove a buddy from a team (soft leave) */ delete: operations["TeamsAdminController_removeMember"]; options?: never; head?: never; /** Change a member role */ patch: operations["TeamsAdminController_changeRole"]; trace?: never; }; "/customers/me/teams/import": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Bulk-import team memberships from a CSV * @description CSV columns: team_slug, buddy_external_user_id, role (+ optional team_name, team_description). `?dry_run=true` validates without writing. */ post: operations["TeamsAdminController_import"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/teams/me": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Get the current buddy’s team, role and members */ get: operations["TeamsWidgetController_me"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/teams/{id}/leave": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Leave a team — blocked for a sole lead until another is promoted */ post: operations["TeamsWidgetController_leave"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/kudo-types": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * List the effective kudo taxonomy * @description Returns the customer authored types, or a virtual generic set (thanks / shoutout / support) when the taxonomy is empty. */ get: operations["KudoTypesAdminController_list"]; put?: never; /** Create a custom kudo type */ post: operations["KudoTypesAdminController_create"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/kudo-types/apply-template": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Apply an industry preset taxonomy * @description mode=replace archives all active types then seeds the template; mode=append adds the template and 409s on any key collision. */ post: operations["KudoTypesAdminController_applyTemplate"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/kudo-types/apply-theme-template": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Apply a theme-aware kudos pack (HTCH-128) * @description Creates a draft/inactive `-` kudos pack. Duplicate keys are skipped (never overwritten); the response carries the created rows and any skipped keys so the Planner can render a clean confirmation before publishing. */ post: operations["KudoTypesAdminController_applyThemeTemplate"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/kudo-types/reorder": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; delete?: never; options?: never; head?: never; /** Persist a new display order */ patch: operations["KudoTypesAdminController_reorder"]; trace?: never; }; "/customers/me/kudo-types/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; /** Archive a kudo type (soft delete) */ delete: operations["KudoTypesAdminController_remove"]; options?: never; head?: never; /** Update a kudo type */ patch: operations["KudoTypesAdminController_update"]; trace?: never; }; "/widget/kudos/types": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * List the effective kudo taxonomy for the composer * @description Returns the workspace authored types, or the virtual generic set (thanks / shoutout / support) when the taxonomy is empty. */ get: operations["KudosWidgetController_types"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/kudos": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Send a kudos to a teammate * @description Records a buddy→buddy recognition transfer. 429s when the sender has reached the workspace daily cap; 200 with duplicate=true when the same kudos is re-sent inside the 60s accident-click window. */ post: operations["KudosWidgetController_send"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/kudos/received": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List the buddy’s most recent received kudos */ get: operations["KudosWidgetController_received"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/kudos/given": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List the buddy’s most recent sent kudos + lifetime count */ get: operations["KudosWidgetController_given"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/group-quests": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List the tenant Group Quests (filter by status / team) */ get: operations["GroupQuestsAdminController_list"]; put?: never; /** Create a Group Quest (status: draft) */ post: operations["GroupQuestsAdminController_create"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/group-quests/{questId}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; /** Delete a Group Quest (draft / cancelled only) */ delete: operations["GroupQuestsAdminController_remove"]; options?: never; head?: never; /** Update a Group Quest — draft fields, active deadline-extension, or cancel */ patch: operations["GroupQuestsAdminController_update"]; trace?: never; }; "/customers/me/group-quests/{questId}/publish": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Publish a draft Group Quest (draft → active) */ post: operations["GroupQuestsAdminController_publish"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/group-quests/{questId}/force-resolve": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** HTCH-56 — manually resolve a Group Quest now (admin watchdog override) */ post: operations["GroupQuestsAdminController_forceResolve"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/group-quests/active": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List the active Group Quests visible to the current buddy */ get: operations["GroupQuestsWidgetController_active"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/group-quests/{id}/join": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Join a Group Quest — idempotent (already_joined on re-join) */ post: operations["GroupQuestsWidgetController_join"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/group-quests/{id}/leave": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Leave a Group Quest — the buddy’s prior contribution stays counted */ post: operations["GroupQuestsWidgetController_leave"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/mentor/availability": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Toggle the current buddy’s mentor availability * @description Flips `mentor_available` on the buddy’s active team membership. 403 `not_a_mentor` when the buddy’s role is not mentor. */ post: operations["MentorWidgetController_setAvailability"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/mentor/team/{id}/mentors": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List a team’s available mentors with contact deep links */ get: operations["MentorWidgetController_teamMentors"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/mentor/sessions": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Self-report a mentoring session * @description Logs an honor-system mentoring session (hours 0.25–8). 403 `not_a_mentor` for non-mentors, 403 `hours_self_report_disabled` when the workspace has the feature off. */ post: operations["MentorWidgetController_logSession"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/mentor/sessions/me": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List the buddy’s recent mentor sessions + hour aggregates */ get: operations["MentorWidgetController_mySessions"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/mentor-visibility/config": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Get the mentor-visibility config */ get: operations["MentorAdminController_getConfig"]; put?: never; post?: never; delete?: never; options?: never; head?: never; /** * Update the mentor-visibility config * @description Validates the contact-URL placeholder allow-list and the PII opt-in. An unknown {{token}} or an {{email}} placeholder without PII opt-in is a 400. */ patch: operations["MentorAdminController_updateConfig"]; trace?: never; }; "/customers/me/mentor-visibility/directory": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List every active mentor across the tenant’s teams */ get: operations["MentorAdminController_directory"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/mentor-visibility/sessions": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; /** * Reset all mentor session logs for the workspace * @description Destructive — wipes every mentor hours log. Intended for the B2B workspace-reset compliance scenario. */ delete: operations["MentorAdminController_resetSessions"]; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/brag/share-profile": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Build the Brag Button "share my profile" payload * @description HTCH-68 — returns the buddy's public Profile Page URL (/p/, minted on first call) plus the tenant brag copy template. */ post: operations["BragWidgetController_shareProfile"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/brag/win-state": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Build the full Brag Button Win-State payload + enabled channels * @description HTCH-60 — returns { payload: BragPayload, enabled_channels }. The widget ceremonies (hatch act-5, evolution) call this once to render the Share CTA. enabled_channels is [] when the Planner toggle is off, so the BragButton self-hides — the single-toggle gate is honored server-side. */ post: operations["BragWidgetController_winState"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/brag/slack-post": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Send a Win-State brag to the tenant Slack/Teams webhook * @description Per-event consent only — the user pressed "Send" in the consent modal. The webhook URL is decrypted server-side; 400 webhook_failed when delivery times out or the endpoint rejects it. */ post: operations["BragWidgetController_slackPost"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/brag/telemetry": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Record one brag funnel event * @description Persists a consent-modal-opened / channel-clicked / completed / dismissed event for the HTCH-61 Planner telemetry dashboard. */ post: operations["BragWidgetController_telemetry"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/brag/config": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Get the Brag Button channel + copy-template config * @description Webhook URLs are returned masked ({ configured, hint }) — the ciphertext envelope never leaves the server. */ get: operations["BragAdminController_getConfig"]; put?: never; post?: never; delete?: never; options?: never; head?: never; /** * Update channel toggles, copy templates and webhook URLs * @description Webhook URLs are encrypted at rest. Copy templates are validated against the placeholder allow-list and per-channel char limits. */ patch: operations["BragAdminController_updateConfig"]; trace?: never; }; "/customers/me/brag/webhook-test": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Send a dummy message to a Slack/Teams webhook URL * @description Verifies a webhook URL before the admin saves it. The message is tagged "Hatched webhook test" so it is obviously not production noise. */ post: operations["BragAdminController_webhookTest"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/brag/funnel": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Brag funnel aggregate over a date window */ get: operations["BragAdminController_funnel"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/brag/by-channel": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Channel × event_kind click / completion matrix */ get: operations["BragAdminController_byChannel"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/brag/telemetry.csv": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Export raw brag telemetry as CSV */ get: operations["BragAdminController_telemetryCsv"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/social-norms/today": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * The buddy's positive-framing team norms for today * @description Returns up to `max_banners_per_session` rendered norms. Norms whose metric falls below the believability floor are silently skipped — a "positive" framing under that floor would be a lie (Yu-kai Ch.9). */ get: operations["SocialNormsWidgetController_today"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/feed/team-events": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * The buddy's team feed — cursor-paginated, newest first * @description Returns the last N SeeSaw Bump events for the buddy’s team plus customer-wide events. `next_cursor` is null when the feed is exhausted. */ get: operations["FeedWidgetController_listTeamEvents"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/feed/team-events/{id}/clap": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Toggle a 👏 clap on a feed item * @description Idempotent — a buddy claps once; a repeat call unclaps. Clapping your own event is rejected with 400 `self_clap_forbidden`. A fresh clap notifies the subject over the `feed.team_event` channel. */ post: operations["FeedWidgetController_clap"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/causes": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List the tenant Cause Counter definitions */ get: operations["CauseAdminController_list"]; put?: never; /** Create a Cause Counter definition */ post: operations["CauseAdminController_create"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/causes/audit": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** HTCH-71 — paginated Cause Counter change history (drawer) */ get: operations["CauseAdminController_audit"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/causes/preview-30-days": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** HTCH-71 — project symbolic units for an unsaved rate config (the drawer rate builder simulation) */ get: operations["CauseAdminController_previewConfig"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/causes/analytics": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** HTCH-107 — F4.5 Humanity Hero admin analytics: customer-wide and per-team contribution rollups, time series, threshold ETA and webhook delivery health (Planner drawer "Analytics" tab) */ get: operations["CauseAdminController_analyticsView"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/causes/analytics.csv": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** HTCH-107 — download the cause analytics as a CSV attachment */ get: operations["CauseAdminController_analyticsCsv"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/causes/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; /** Delete a Cause Counter definition */ delete: operations["CauseAdminController_remove"]; options?: never; head?: never; /** Update a Cause Counter definition */ patch: operations["CauseAdminController_update"]; trace?: never; }; "/customers/me/causes/{id}/draft": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Stage draft edits for a published Cause Counter definition */ post: operations["CauseAdminController_draft"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/causes/{id}/preview-30-days": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Project symbolic units from the last 30 days of eligible events for a saved cause */ get: operations["CauseAdminController_preview"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/causes/{id}/webhook": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** HTCH-106 — F4.5 cause webhook config + recent delivery attempts */ get: operations["CauseAdminController_getWebhook"]; put?: never; post?: never; delete?: never; options?: never; head?: never; /** HTCH-106 — F4.5 set the cause webhook URL, secret and threshold step */ patch: operations["CauseAdminController_updateWebhook"]; trace?: never; }; "/customers/me/causes/{id}/webhook/test": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** HTCH-106 — F4.5 send a test cause.threshold_reached event and return the delivery outcome inline */ post: operations["CauseAdminController_testWebhook"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/causes/counters": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List symbolic cause counters for the current buddy / team / tenant */ get: operations["CauseWidgetController_counters"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/causes/surfaces": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** HTCH-70 — cause counters grouped per opted-in surface (banner / buddy strip / profile) */ get: operations["CauseWidgetController_surfaces"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/foundations": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List the tenant's active foundation selections — read-only for widget rendering. */ get: operations["FoundationsController_widgetList"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/council/proposals": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** The Council narrative-proposal moderation queue */ get: operations["CouncilAdminController_list"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/council/proposals/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; delete?: never; options?: never; head?: never; /** Approve or reject a pending proposal */ patch: operations["CouncilAdminController_moderate"]; trace?: never; }; "/customers/me/council/narrative/slots/{slot}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; /** Promote an approved proposal into a live narrative slot */ put: operations["CouncilAdminController_promote"]; post?: never; /** Retire the live proposal in a slot and restore the default copy */ delete: operations["CouncilAdminController_revert"]; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/council/proposals/mine": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** The buddy's own narrative proposals plus Council standing and quota */ get: operations["CouncilWidgetController_mine"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/council/proposals": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Submit a narrative proposal (Council members only) */ post: operations["CouncilWidgetController_create"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/founding-cohort/preview": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Project how many buddies the Founding Cohort config would mark. Optional query params preview an unsaved mode/threshold. */ get: operations["FoundingCohortAdminController_preview"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/founding-cohort/backfill": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Retroactively mark every currently-eligible buddy (idempotent) */ post: operations["FoundingCohortAdminController_backfill"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/founding-cohort/audit": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Paginated Founding Cohort assignment history */ get: operations["FoundingCohortAdminController_audit"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/founding-cohort/audit/export.csv": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Export the full Founding Cohort assignment history as CSV */ get: operations["FoundingCohortAdminController_exportAuditCsv"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/founding-cohort/status": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Founding Cohort status for the current buddy */ get: operations["FoundingCohortWidgetController_status"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/notifications": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Paginated notification feed for the current buddy */ get: operations["NotificationWidgetController_list"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/notifications/unread-count": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Unread, non-dismissed notification count (badge) */ get: operations["NotificationWidgetController_unreadCount"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/notifications/dismiss-all": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Read + dismiss every notification for the buddy */ post: operations["NotificationWidgetController_dismissAll"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/notifications/{id}/read": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Mark a single notification read */ post: operations["NotificationWidgetController_read"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/notifications/{id}/dismiss": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Read + dismiss a single notification (HTCH-76) */ post: operations["NotificationWidgetController_dismiss"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/notifications/{id}/snooze": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Snooze a notification for a number of hours (HTCH-76) */ post: operations["NotificationWidgetController_snooze"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/profile/sunk-cost-summary": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Sunk-Cost 'Your journey so far' summary for the current buddy */ get: operations["SunkCostController_summary"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/profile/sunk-cost-summary/acknowledge": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Acknowledge the Sunk-Cost panel on first open — fires the paired White Hat celebration.milestone_acknowledged (idempotent per buddy) */ post: operations["SunkCostController_acknowledge"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/buddy/pause": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Put the current buddy on vacation until a date */ post: operations["VacationWidgetController_pause"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/buddy/resume": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** End the current buddy vacation early */ post: operations["VacationWidgetController_resume"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/buddy/vacation-status": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Current buddy vacation status */ get: operations["VacationWidgetController_status"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/vacation/analytics": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Vacation usage analytics for the Planner drawer panel */ get: operations["VacationAdminController_analytics"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/streak-at-risk/analytics": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Streak-at-risk volume + recovery analytics for the Planner drawer */ get: operations["StreakWatchdogAdminController_analytics"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/profile/history": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Visual Grave history — faded lost streaks + reclaimable items */ get: operations["ProfileHistoryController_getHistory"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/profile/history/items/{id}/reclaim": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Earn a relinquished starter-rare back — fires recovery.streak_restored */ post: operations["ProfileHistoryController_reclaim"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/mystery-box/state": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Mystery Box state — eligible / capped / locked */ get: operations["MysteryBoxController_state"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/mystery-box/claim": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Open the Mystery Box — 409 with next_eligible_at when the daily cap is spent */ post: operations["MysteryBoxController_claim"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/surprise-drops": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List surprise-drop definitions for the Planner */ get: operations["SurpriseDropsController_list"]; put?: never; /** Create a custom surprise drop */ post: operations["SurpriseDropsController_create"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/surprise-drops/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; /** Delete a custom surprise drop */ delete: operations["SurpriseDropsController_remove"]; options?: never; head?: never; /** Update a surprise drop — global templates edit copy-on-write */ patch: operations["SurpriseDropsController_update"]; trace?: never; }; "/customers/me/boosters/grant": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Grant a catalog booster to a buddy (admin one-off) */ post: operations["BoostersAdminController_grant"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/boosters/active": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** The buddy’s currently active boosters */ get: operations["BoostersController_active"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/boosters/catalog": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Buyable boosters for this tenant */ get: operations["BoostersController_catalog"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/boosters/purchase": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Buy a catalog booster — 400 insufficient_balance when too few coins */ post: operations["BoostersController_purchase"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/lotteries": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List lottery definitions for the Planner */ get: operations["LotteriesController_list"]; put?: never; /** Create a lottery definition */ post: operations["LotteriesController_create"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/lotteries/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; /** Soft-delete a lottery (history stays queryable) */ delete: operations["LotteriesController_remove"]; options?: never; head?: never; /** Update a lottery definition */ patch: operations["LotteriesController_update"]; trace?: never; }; "/customers/me/lotteries/{id}/draws": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Past draw history + analytics for a lottery */ get: operations["LotteriesController_draws"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/lotteries/{id}/preview-next-draw": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Current-period entry count + next draw time for the preview card */ get: operations["LotteriesController_previewNextDraw"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/lotteries/{id}/simulate-draw": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Simulate a draw with the current entries — no rewards granted */ post: operations["LotteriesController_simulateDraw"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/lottery/active-entries": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** The buddy's live lottery entries with their next-draw time */ get: operations["LotteryWidgetController_activeEntries"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/lottery/last-win": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** The buddy's most recent lottery win, if any */ get: operations["LotteryWidgetController_lastWin"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/profile-templates": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List profile-page templates (system + custom) */ get: operations["ProfileTemplateController_list"]; put?: never; /** Create a profile-page template */ post: operations["ProfileTemplateController_create"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/profile-templates/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; /** Delete a profile-page template */ delete: operations["ProfileTemplateController_remove"]; options?: never; head?: never; /** Update a profile-page template */ patch: operations["ProfileTemplateController_update"]; trace?: never; }; "/customers/me/profile-templates/apply-bulk": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Assign a template to many buddies in one statement */ post: operations["ProfileTemplateController_applyBulk"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/leagues/me": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** The buddy's live league standing — tier, cohort, countdown */ get: operations["LeagueWidgetController_me"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/leagues/boss-fight": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** The season's Boss Fight challenge — progress, target, leaderboard */ get: operations["LeagueWidgetController_bossFight"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/leagues/seasons/latest/highlights/me": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** The buddy's latest closed season-closing highlights */ get: operations["LeagueHighlightsController_latest"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/leagues/seasons/{seasonId}/highlights/me": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** The buddy's personalized season-closing highlights */ get: operations["LeagueHighlightsController_me"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/leagues/off-season/status": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** The off-season window — Mystery Box boost, wardrobe drops, scouting quest */ get: operations["OffSeasonController_status"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/leagues/off-season/scouting-quest": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Start the cohort pre-season scouting quest with a prediction */ post: operations["OffSeasonController_startScoutingQuest"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/leagues/off-season/scouting-quest/join": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Join the cohort's already-started scouting quest */ post: operations["OffSeasonController_joinScoutingQuest"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/leagues/config": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Full tier ladder, cohort/cadence config and season state */ get: operations["LeagueAdminController_getConfig"]; put?: never; post?: never; delete?: never; options?: never; head?: never; /** Update the cohort maths, season cadence and off-season window */ patch: operations["LeagueAdminController_updateConfig"]; trace?: never; }; "/customers/me/leagues/tiers": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; /** Bulk-replace the tier ladder — 409 if a removed tier still has buddies */ put: operations["LeagueAdminController_replaceTiers"]; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/leagues/seasons/preview": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Project the next three season windows (no write) */ post: operations["LeagueAdminController_previewSeasons"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/leagues/seasons": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Schedule the next upcoming season */ post: operations["LeagueAdminController_scheduleSeason"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/leagues/seasons/{seasonId}/force-close": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Manually trigger the rollover for a season (audit logged) */ post: operations["LeagueAdminController_forceClose"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/public/hall-of-fame/{tenantSlug}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List a tenant's finalized Hall of Fame seasons */ get: operations["HallOfFameController_getList"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/public/hall-of-fame/{tenantSlug}/{seasonId}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** One finalized season in the public Hall of Fame */ get: operations["HallOfFameController_getSeason"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/public/hall-of-fame-index": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List public Hall of Fame season URLs (paged) */ get: operations["HallOfFameIndexController_getIndex"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/hexad-survey/questions": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * List Hexad survey question metadata * @description Returns the 24 question keys + axis assignment. Verbatim Marczewski text is loaded by the widget from its tenant-installable copy bundle, not from this endpoint. */ get: operations["HexadSurveyController_questions"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/hexad-survey/responses": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Submit (or replace) the buddy Hexad survey response * @description UPSERT keyed by buddy_id. Re-takes overwrite the previous row in place; audience_key + customer_id are sourced from the buddy row so the widget can not spoof them. */ post: operations["HexadSurveyController_submit"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/widget/hexad-survey/me": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Fetch the current buddy Hexad response * @description Lets the survey widget render the user's own profile after they submit; absent rows resolve to `null`. */ get: operations["HexadSurveyController_me"]; put?: never; post?: never; /** * Delete the buddy raw response (GDPR / consent withdrawal) * @description Removes the raw answers + derived scores. The nightly aggregator picks up the lower response_count on the next run; audience-level aggregates are preserved. */ delete: operations["HexadSurveyController_deleteMe"]; options?: never; head?: never; patch?: never; trace?: never; }; "/marketing/cta": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** * Record a public marketing CTA click * @description Unauthenticated browser beacon endpoint used by the marketing site to track CTA intent and first-touch attribution. */ post: operations["MarketingAnalyticsController_recordCtaClick"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/showrooms": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List the customer’s Showroom pages */ get: operations["ShowroomAdminController_list"]; put?: never; /** Create a Showroom page from a template */ post: operations["ShowroomAdminController_create"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/showrooms/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Fetch one Showroom page (admin lens) */ get: operations["ShowroomAdminController_getOne"]; put?: never; post?: never; delete?: never; options?: never; head?: never; /** Update layout / header / visibility */ patch: operations["ShowroomAdminController_update"]; trace?: never; }; "/customers/me/showrooms/{id}/publish": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Publish a Showroom (status → published) */ post: operations["ShowroomAdminController_publish"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/showrooms/{id}/unpublish": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Unpublish a Showroom (status → draft) */ post: operations["ShowroomAdminController_unpublish"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/showrooms/{id}/regenerate-qr": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Rotate the QR token, invalidating any printed code */ post: operations["ShowroomAdminController_regenerateQr"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/showrooms/{id}/archive": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Archive a Showroom (hidden from list, kept for audit) */ post: operations["ShowroomAdminController_archive"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/players/{buddyId}/award": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** HR Award Drawer — grant a badge / skill_event / coin / kudo / forced evolution to a buddy */ post: operations["ShowroomAdminController_award"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/players/{buddyId}/awards": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Recent HR awards for a buddy (audit lens) */ get: operations["ShowroomAdminController_listPlayerAwards"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/awards": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Customer-wide HR award audit log */ get: operations["ShowroomAdminController_listAudit"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/public/showroom/{slug}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Resolve a Showroom slug to its public view */ get: operations["PublicShowroomController_getPublicShowroom"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/public/showroom/{slug}/qr": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Return the QR payload for a Showroom (url + token). PNG rendering is client-side in v1. */ get: operations["PublicShowroomController_getQrPayload"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/hosted-surfaces": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** List the customer’s hosted surfaces */ get: operations["HostedSurfacesAdminController_list"]; put?: never; /** Create a hosted surface from a template */ post: operations["HostedSurfacesAdminController_create"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/hosted-surfaces/{id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Fetch one hosted surface (admin lens) */ get: operations["HostedSurfacesAdminController_getOne"]; put?: never; post?: never; delete?: never; options?: never; head?: never; /** Update name / theme / layout / mode / widget version */ patch: operations["HostedSurfacesAdminController_update"]; trace?: never; }; "/customers/me/hosted-surfaces/{id}/readiness": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Per-widget content readiness for the go-live checklist * @description Mirrors the public layout resolver: widgets without tenant content (no active streak, no marketplace items, no badge definitions) are hidden from players — this endpoint tells the operator which and why before they share the URL. */ get: operations["HostedSurfacesAdminController_readiness"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/hosted-surfaces/{id}/logo": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Upload a hosted surface logo and attach it to the public shell theme */ post: operations["HostedSurfacesAdminController_uploadLogo"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/hosted-surfaces/{id}/publish": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post: operations["HostedSurfacesAdminController_publish"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/hosted-surfaces/{id}/unpublish": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post: operations["HostedSurfacesAdminController_unpublish"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/hosted-surfaces/{id}/archive": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post: operations["HostedSurfacesAdminController_archive"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/hosted-surfaces/{id}/players": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get: operations["HostedSurfacesAdminController_listPlayers"]; put?: never; /** Add a player. Provide buddy_id to link an existing buddy or display_name to mint a new one. */ post: operations["HostedSurfacesAdminController_createPlayer"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/hosted-surfaces/{id}/players/{playerId}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post?: never; delete?: never; options?: never; head?: never; patch: operations["HostedSurfacesAdminController_updatePlayer"]; trace?: never; }; "/customers/me/hosted-surfaces/{id}/players/{playerId}/regenerate-access": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post: operations["HostedSurfacesAdminController_regenerateAccess"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/hosted-surfaces/{id}/players/{playerId}/access-code": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Re-view a player’s current access code + QR token without rotating them. Returns available:false for players created before encrypted-at-rest storage existed — regenerate once to mint a re-viewable copy. Audited. */ get: operations["HostedSurfacesAdminController_revealAccess"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/hosted-surfaces/{id}/recipes": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get: operations["HostedSurfacesAdminController_listRecipes"]; put?: never; post: operations["HostedSurfacesAdminController_upsertRecipe"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/customers/me/hosted-surfaces/{id}/recipes/{key}/run": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; post: operations["HostedSurfacesAdminController_runRecipe"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/public/hosted-surfaces/{slug}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** Resolve a hosted surface slug to its public config (theme, layout, loader URL, auth requirement). */ get: operations["PublicHostedSurfacesController_getPublic"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/public/hosted-surfaces/{slug}/session": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; get?: never; put?: never; /** Exchange an access code or QR token for a short-lived widget session token. */ post: operations["PublicHostedSurfacesController_startSession"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/health": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Health check * @description Full health check of all service dependencies (database, Redis, queues, image provider) */ get: operations["HealthController_check"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/health/ready": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Readiness check * @description Kubernetes-style readiness probe. Returns 200 when all critical dependencies are up, 503 otherwise — load balancers use this to stop routing traffic to unhealthy instances. */ get: operations["HealthController_readiness"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/health/live": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Liveness check * @description Kubernetes-style liveness probe confirming the process is alive */ get: operations["HealthController_liveness"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/health/version": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Build metadata * @description Returns the API version, git commit SHA, and ISO build timestamp. Useful as a deployment fingerprint — pin a partner client against a known build, or compare expected vs actual when debugging a rollout. */ get: operations["HealthController_version"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/healthz": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Liveness probe (top-level alias) * @description Kubernetes-style liveness probe. Bare path so probes do not need the API prefix. */ get: operations["ProbeController_healthz"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/readyz": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Readiness probe (top-level alias) * @description Returns 200 only when database and Redis are both reachable. Bare path mirrors `/api/v1/health/ready`. */ get: operations["ProbeController_readyz"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; "/version": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; /** * Build metadata (top-level alias) * @description Returns API version, git commit SHA, and ISO build timestamp. Useful as a deployment fingerprint. */ get: operations["ProbeController_version"]; put?: never; post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; }; ``` ### `webhooks` ```ts export type webhooks = Record; ``` ### `components` ```ts export type components = { schemas: { UpdateCustomerDto: { /** * @description Customer display name * @example Acme Corp */ name?: string; }; CustomerFeaturesDto: { /** @description Enable marketplace features */ marketplace?: boolean; /** @description Enable token currency */ tokens?: boolean; /** @description Enable evolution stages */ evolution?: boolean; /** @description Enable badges */ badges?: boolean; /** @description Enable streaks */ streaks?: boolean; /** @description Enable the Teams social graph */ teams?: boolean; /** @description Enable peer recognition (Kudos) */ kudos?: boolean; /** @description Enable Group Quest widgets */ group_quest?: boolean; /** @description Enable mentorship widgets */ mentorship?: boolean; /** @description Enable SeeSaw Bump feed widgets */ seesaw_bump?: boolean; /** @description Enable Mystery Box surprise-drop widgets */ mystery_box?: boolean; /** @description Enable LEAGUES seasonal competition widgets */ leagues?: boolean; /** @description Enable Council prestige UGC widgets */ council?: boolean; /** @description Enable Gamification Planner admin surface */ gamification_planner?: boolean; /** @description Enable guided path journeys */ paths?: boolean; /** @description Enable time-based skill decay sweeps */ decay?: boolean; }; BuddySharingSettingsDto: { /** @description Enable public buddy sharing */ enabled?: boolean; /** @description Show the tenant name on public share cards */ show_tenant_name?: boolean; /** * @description Absolute HTTP(S) URL for the primary share-page CTA * @example https://app.example.com/signup */ cta_url?: string; /** * @description Verified share hostname, without protocol or path * @example share.example.com */ custom_domain?: Record; /** @description Whether custom_domain DNS ownership has been verified */ custom_domain_verified?: boolean; }; UpdateSettingsDto: { /** * @description Visual style for generated creatures * @example cute * @enum {string} */ creature_style?: "cute" | "fantasy" | "scifi" | "nature" | "minimal" | "custom"; /** * @description Custom prompt for creature image generation * @example A friendly dragon with blue scales */ custom_prompt?: string; /** * @description Widget color theme * @example light * @enum {string} */ widget_theme?: "light" | "dark" | "custom"; /** * @description Default widget layout size * @example medium * @enum {string} */ widget_size?: "small" | "medium" | "large"; /** @description Custom CSS injected into Hatched widget shadow roots */ widget_custom_css?: string; /** @description Structured widget theme tokens generated from onboarding or edited in the dashboard */ widget_theme_config?: Record; /** * @description Language code for widget UI * @example en */ language?: string; /** * @description IANA timezone identifier * @example Europe/Istanbul */ timezone?: string; /** * @description Show buddy name in the widget * @example true */ widget_show_name?: boolean; /** * @description Show skill levels in the widget * @example true */ widget_show_skills?: boolean; /** * @description Show coin balance in the widget * @example true */ widget_show_coins?: boolean; /** * @description Show earned badges in the widget * @example false */ widget_show_badges?: boolean; /** * @description Show evolution progress in the widget * @example true */ widget_show_evolution?: boolean; /** * @description Show the XP chip in the widget * @example true */ widget_show_xp?: boolean; /** * @description Show leaderboard rank in the widget * @example false */ widget_show_leaderboard_rank?: boolean; /** * @description Enable leaderboard feature * @example true */ widget_enable_leaderboard?: boolean; /** * @description Browser origins allowed to call widget runtime endpoints for this customer * @example [ * "https://app.example.com", * "http://localhost:4002" * ] */ widget_allowed_origins?: string[]; /** * @description Image generation model identifier * @example dall-e-3 */ image_model?: string; /** * @description Image generation quality tier * @example standard * @enum {string} */ image_tier?: "standard" | "premium"; /** * @description Maximum number of buddies a single user can own * @example 5 */ max_buddies_per_user?: number; /** * @description Maximum number of active eggs a single user can have * @example 3 */ max_active_eggs_per_user?: number; /** @description Feature toggles (marketplace/tokens/evolution/badges/streaks/paths/decay) */ features?: components["schemas"]["CustomerFeaturesDto"]; /** @description Buddy-share public page and CTA preferences */ sharing?: components["schemas"]["BuddySharingSettingsDto"]; /** @description Narrative brand/visual prompt used to seed all AI-generated assets (badge icons, marketplace item art). Editable at any time. */ theme_prompt?: string; /** @description Set true when the user manually edits theme_prompt */ theme_prompt_locked?: boolean; }; AudienceDto: { /** @example student */ key: string; /** @example Students */ label: string; /** @description Short human-readable description used in plan prompts. */ description?: string; }; UpdateAudiencesDto: { audiences: components["schemas"]["AudienceDto"][]; }; RegisterCustomerDto: { /** * @description Customer or company name * @example Acme Corp */ name: string; /** * Format: email * @description Email address for the account * @example admin@acme.com */ email: string; /** * @description Account password (min 8 characters) * @example S3cur3P@ss! */ password: string; /** * @description Must be true to confirm acceptance of current Hatched Terms and Privacy Policy * @example true */ terms_accepted: boolean; /** * @description Referral attribution token, e.g. share:, marketing, demo, or demo_try * @example demo_try */ ref?: string; }; LoginDto: { /** * Format: email * @description Account email address * @example admin@acme.com */ email: string; /** * @description Account password * @example S3cur3P@ss! */ password: string; }; ChangePasswordDto: { current_password: string; new_password: string; }; PasswordUpdatedResponseDto: { /** @example true */ updated: boolean; /** @description Fresh dashboard JWT returned when the currently authenticated user changes their own password. */ token?: string; }; RequestPasswordResetDto: { /** Format: email */ email: string; }; PasswordResetRequestedResponseDto: { /** @example If that account exists, password reset instructions are available. */ message: string; /** @description One-time reset link. Only returned for local dashboard origins so local QA can complete the flow without an email sender. */ reset_url?: string; /** * Format: date-time * @description Expiry of the reset token. Present only when reset_url is. */ expires_at?: string; }; ResetPasswordDto: { token: string; new_password: string; }; VerifyEmailDto: { /** @description One-time email verification token */ token: string; }; EmailVerificationResponseDto: { /** @example true */ verified: boolean; }; EmailVerificationRequestedResponseDto: { /** @example Verification instructions are available. */ message: string; /** @description One-time verification link. Only returned for local dashboard origins so local QA can complete the flow without an email sender. */ verification_url?: string; /** * Format: date-time * @description Expiry of the verification token. Present only when verification_url is. */ expires_at?: string; /** @description True when the authenticated email is already verified. */ already_verified?: boolean; }; CreateApiKeyDto: { /** * @description Human-readable label for the API key * @example Production key */ label?: string; }; OperationResponseDto: { /** * @description Unique operation identifier * @example 550e8400-e29b-41d4-a716-446655440000 */ id: string; /** * @description Operation type * @example hatch */ type: string; /** * @description Current operation status * @example completed * @enum {string} */ status: "pending" | "processing" | "completed" | "failed" | "cancelled"; /** * @description Type of the resource associated with this operation * @example buddy */ resource_type?: string | null; /** * @description Identifier of the resource associated with this operation * @example 550e8400-e29b-41d4-a716-446655440002 */ resource_id?: string | null; /** * @description Result payload on success * @example { * "buddy_id": "550e8400-e29b-41d4-a716-446655440002" * } */ result?: Record | null; /** * @description Error payload on failure * @example { * "code": "IMAGE_GENERATION_FAILED", * "message": "Timeout" * } */ error?: Record | null; /** * Format: date-time * @description Timestamp when the operation was created * @example 2026-04-09T12:00:00.000Z */ created_at: string; /** * Format: date-time * @description Timestamp when the operation was last updated * @example 2026-04-09T12:05:00.000Z */ updated_at: string; }; PlayerZeroBuddyResponseDto: { /** * @description Buddy UUID for the workspace demo player. * @example b3d7c8a0-1234-4f5e-9abc-def012345678 */ id: string; /** * @description Reserved external user id used by dashboard previews. * @example player-0 */ user_id: string; /** * @description Display name shown in preview surfaces. * @example Player Zero */ name: string; /** * @description Audience segment attached to the buddy. * @example default */ audience: string; /** * @description Current evolution stage used by the widget preview. * @example 1 */ evolution_stage: number; /** * @description Image URL rendered by widget previews. A newly-created Player Zero may return a safe placeholder while the background base render completes. * @example https://demo.staging.hatched.live/fern/stage-1.webp */ image_url: string; }; PlayerZeroResponseDto: { /** * @description True only when Player Zero was created by this request. * @example true */ created: boolean; buddy: components["schemas"]["PlayerZeroBuddyResponseDto"]; }; PlayerZeroStatusResponseDto: { /** * @description Whether the workspace demo player has been provisioned. * @example true */ exists: boolean; /** * @description Whether any active buddy in the workspace has completed the hatch ceremony (Player Zero or a real player — e.g. on a hosted surface). * @example false */ hatched: boolean; /** * @description Buddy UUID, or null when Player Zero does not exist yet. * @example b3d7c8a0-1234-4f5e-9abc-def012345678 */ buddyId: string | null; }; CreateEmbedTokenDto: { /** * @description UUID of the buddy to render in the widget * @example b3d7c8a0-1234-4f5e-9abc-def012345678 */ buddy_id: string; /** * @description Identifier of the end user viewing the widget * @example usr_67890 */ user_id: string; /** * @description Embed token lifetime in seconds. Defaults to 86400. * @example 3600 */ ttl_seconds?: number; }; CreateEmbedTokenResponseDto: { /** * @description The signed read-only embed JWT to hand to the widget loader. * @example eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... */ token: string; /** * @description ISO 8601 timestamp when the embed token expires. * @example 2026-05-31T12:00:00.000Z */ expires_at: string; /** * @description Always `read-only` — embed tokens never carry write scopes. * @example read-only */ mode: string; }; CreateSessionTokenDto: { /** * @description UUID of the buddy for this widget session * @example b3d7c8a0-1234-4f5e-9abc-def012345678 */ buddy_id: string; /** * @description Identifier of the end user for this session * @example usr_67890 */ user_id: string; /** * @description Permission scopes granted to this widget session * @example [ * "read", * "marketplace:purchase" * ] */ scopes: ("read" | "events:track" | "marketplace:purchase" | "items:equip" | "marketplace:browse" | "kudos:send" | "quests:join" | "mysterybox:claim" | "council:propose" | "survey:submit" | "feed:react" | "buddy:write" | "teams:manage")[]; /** * @description Session lifetime in seconds. Defaults to 3600. * @example 900 */ ttl_seconds?: number; }; CreateSessionTokenResponseDto: { /** * @description The signed session JWT to hand to the widget loader. * @example eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... */ token: string; /** * @description UUID of the persisted widget session, used to revoke it. * @example b3d7c8a0-1234-4f5e-9abc-def012345678 */ session_id: string; /** * @description ISO 8601 timestamp when the session token expires. * @example 2026-05-30T12:00:00.000Z */ expires_at: string; /** * @description The permission scopes granted to this session. * @example [ * "read", * "events:track", * "kudos:send" * ] */ scopes: ("read" | "events:track" | "marketplace:purchase" | "items:equip" | "marketplace:browse" | "kudos:send" | "quests:join" | "mysterybox:claim" | "council:propose" | "survey:submit" | "feed:react" | "buddy:write" | "teams:manage")[]; }; VerifyInstallationDto: { /** * @description Page URL to probe for the Hatched loader. Defaults to the first configured allowed origin when omitted. * @example https://app.acme.com */ url?: string; }; VerifyInstallationResponseDto: { /** * @description The final URL actually fetched, after any redirects. * @example https://app.acme.com */ checked_url: string; /** @description Whether the page could be fetched at all. */ reachable: boolean; /** @description Whether the Hatched loader was detected in the served HTML. */ found: boolean; /** * @description Which signals matched, e.g. `loader script`, `mount target`. * @example [ * "loader script", * "mount target" * ] */ signals: string[]; /** @description Human-readable outcome or failure reason, safe to surface. */ detail: string; }; RecordShareEventDto: Record; WidgetBuddyHatchedDto: Record; BuddyHatchedBuddyDto: { /** @description Buddy UUID. */ id: string; /** @description Buddy display name (possibly just set by the ceremony). */ name: string | null; /** * @description True once the hatch ceremony completion is persisted. * @example true */ hatch_ceremony_seen: boolean; }; BuddyHatchedResponseDto: { /** @example true */ ok: boolean; buddy: components["schemas"]["BuddyHatchedBuddyDto"]; }; WidgetBuddySeoDto: Record; WidgetBuddyProfileDto: Record; WidgetRenderedDto: { /** * @description Widget IDs that successfully mounted on this page view. * @example [ * "buddy" * ] */ widgets?: string[]; /** * @description Loader semantic version. * @example 0.4.2 */ loader_version?: string; /** * @description Opaque loader build identifier. * @example 20260614.1 */ build_id?: string; }; TrackEventDto: { /** * @description Event type. Must already be registered for the buddy’s audience via POST /api/v1/event-types. * @example lesson_completed */ type: string; /** * @description Client-generated unique event identifier. When omitted the server derives one from buddy + type + timestamp. Used for idempotency — send the same event_id to retry without double-counting. * @example evt_browser_abc123 */ event_id?: string; /** * @description ISO 8601 timestamp. When omitted the server uses its current time. * @example 2026-04-22T10:00:00Z */ occurred_at?: string; /** * @description Additional properties associated with the event * @example { * "lesson_id": "lesson_42", * "score": 95 * } */ properties?: Record; }; PurchaseItemDto: Record; EquipItemsDto: Record; PreviewOutfitDto: Record; SaveOutfitDto: Record; ApplyPresetDto: { /** * @description Key identifying the preset configuration to apply * @example language-learning */ preset_key: string; }; CreateConfigVersionDto: { /** * @description Configuration snapshot object containing all settings for this version * @example { * "coin_rules": [], * "skill_rules": [], * "badge_definitions": [] * } */ snapshot?: Record; }; UpdateConfigVersionDto: { /** * @description Updated configuration snapshot object * @example { * "coin_rules": [], * "skill_rules": [], * "badge_definitions": [] * } */ snapshot?: Record; }; CreateEggDto: { /** * @description Unique identifier of the end-user who owns the egg * @example user_abc123 */ user_id: string; /** * @description Arbitrary metadata to attach to the egg * @example { * "source": "onboarding", * "campaign": "spring2026" * } */ metadata?: Record; }; EggResponseDto: { /** * @description Unique egg identifier * @example 550e8400-e29b-41d4-a716-446655440000 */ egg_id: string; /** * @description Current egg status * @example incubating * @enum {string} */ status: "incubating" | "ready" | "hatched" | "cancelled"; /** * @description Visual variant number for the egg appearance * @example 3 */ visual_variant: number; /** * @description Configuration version used for this egg * @example 550e8400-e29b-41d4-a716-446655440001 */ config_version_id: string; /** * @description Owner user identifier * @example user_abc123 */ user_id: string; /** * @description Identifier of the buddy hatched from this egg. Present once status === "hatched". * @example 550e8400-e29b-41d4-a716-446655440002 */ buddy_id?: Record | null; /** * @description Arbitrary metadata attached to the egg * @example { * "source": "onboarding" * } */ metadata?: Record | null; /** * Format: date-time * @description Timestamp when the egg was created * @example 2026-04-09T12:00:00.000Z */ created_at: string; }; UpdateEggStatusDto: { /** * @description New egg status * @example ready * @enum {string} */ status: "ready" | "cancelled"; }; EggStatusChangeResponseDto: { /** * @description Egg identifier * @example 550e8400-e29b-41d4-a716-446655440000 */ egg_id: string; /** * @description New egg status after the update * @example ready */ status: string; /** * @description Egg status before the update * @example incubating */ previous_status: string; }; UpdateBuddyDto: { /** * @description New display name for the buddy * @example Sparky */ name?: string; }; SkillUpdateDto: { /** * @description Skill key (lowercase alphanumeric with underscores) * @example vocabulary */ key: string; /** * @description Action to perform on the skill value * @example increase * @enum {string} */ action: "increase" | "decrease" | "set"; /** * @description Amount to increase or decrease (used with increase/decrease actions) * @example 10 */ amount?: number; /** * @description Absolute value to set (used with set action) * @example 50 */ value?: number; }; UpdateBuddySkillsDto: { /** @description Array of skill update operations */ updates: components["schemas"]["SkillUpdateDto"][]; }; EarnCoinsDto: { /** * @description Action type, must be "earn" * @example earn * @enum {string} */ action: "earn"; /** * @description Number of coins to earn * @example 10 */ amount: number; /** * @description Reason for earning coins * @example completed_lesson */ reason: string; /** * @description Optional reference ID for tracking the source event * @example lesson_abc123 */ reference_id?: string; }; SpendCoinsDto: { /** * @description Number of coins to spend * @example 5 */ amount: number; /** * @description Optional item ID associated with the purchase * @example hat_red_01 */ item_id?: string; /** * @description Reason for spending coins * @example purchase_accessory */ reason: string; }; AwardBadgeDto: { /** * @description Key of the badge definition to award * @example first_hatch */ badge_key: string; /** * @description Optional reason for awarding the badge * @example Manually awarded by admin */ reason?: string; }; UnlockItemDto: { /** * @description UUID of the item to unlock * @example b3d7c8a0-1234-4f5e-9abc-def012345678 */ item_id: string; /** * @description Reason for unlocking the item * @example Achievement reward */ reason?: string; }; TokenTransactionDto: { /** * @description Customer-defined token key (e.g. "gems", "xp"). Must match a configured token_config row. * @example gems */ token_type: string; /** * @description Transaction direction. `earn` always succeeds for active tokens; `spend` fails with progression_not_spendable for progression tokens. * @example earn * @enum {string} */ action: "earn" | "spend"; /** * @description Number of tokens to transact * @example 1 */ amount: number; /** * @description Reason for the token transaction * @example lesson_completed */ reason: string; /** * @description Optional reference ID for tracking the source event * @example evt_abc123 */ reference_id?: string; }; CreateShareDto: Record; ShareEventDto: Record; UpdateSharingDto: Record; PublicShareEventDto: Record; CreateSkillSetDto: { /** * @description Name of the skill set * @example Language Skills */ name: string; /** @description Array of skill definitions (1-10 skills) */ skills: unknown[][]; }; GenerateSkillIconDto: { /** * @description Skill label — drives the subject of the generated icon (e.g. "Writing", "Listening") * @example Listening */ label: string; /** * @description Optional skill description for additional context * @example Tracks how often the learner focuses on audio-based practice */ description?: string; /** * @description Visual treatment style * @default flat * @enum {string} */ style: "flat" | "line" | "duotone" | "glyph" | "isometric"; /** @description Free-form hint to nudge the composition (e.g. "a single headphone, minimalist") */ hint?: string; }; UpdateSkillSetDto: { /** * @description Name of the skill set * @example Language Skills */ name?: string; /** @description Array of skill definitions (1-10 skills) */ skills?: unknown[][]; }; CreateSkillRuleDto: { /** * @description Trigger event key that activates this rule * @example lesson_completed */ trigger: string; /** * @description Skill key that receives XP when this rule triggers * @example vocabulary */ skill_key: string; /** * @description Amount of XP to award * @example 10 */ amount: number; /** * @description Maximum times this rule can trigger per day * @example 5 */ daily_limit?: number; /** * @description Whether this skill rule is active * @example true */ active?: boolean; }; UpdateSkillRuleDto: { /** * @description Trigger event key that activates this rule * @example lesson_completed */ trigger?: string; /** * @description Skill key that receives XP when this rule triggers * @example vocabulary */ skill_key?: string; /** * @description Amount of XP to award * @example 15 */ amount?: number; /** * @description Maximum times this rule can trigger per day * @example 5 */ daily_limit?: number; /** * @description Whether this skill rule is active * @example true */ active?: boolean; }; CreateSkillDecayRuleDto: { /** * @description Skill key whose level decays on the chosen cadence * @example vocabulary */ skill_key: string; /** * @description How often the decay is applied * @example daily * @enum {string} */ cadence: "daily" | "weekly" | "monthly"; /** * @description Amount subtracted from the skill level each cadence period * @example 2 */ amount: number; /** * @description Lower bound — the skill level will never decay below this value. Defaults to 0. * @example 20 */ floor_level?: number; /** * @description Number of days after a buddy is created during which decay is suppressed. * @example 7 */ grace_days?: number; /** * @description Decay only applies when the current skill level is strictly greater than this threshold. Use to keep low-skill users from being further punished. * @example 50 */ apply_only_above?: number; /** * @description Audience scope for this rule. Defaults to "default". * @example default */ audience?: string; /** * @description Whether the rule is active. Defaults to false so a draft rule does not start decaying production users on creation. * @example false */ active?: boolean; }; UpdateSkillDecayRuleDto: { skill_key?: string; /** @enum {string} */ cadence?: "daily" | "weekly" | "monthly"; amount?: number; floor_level?: number; grace_days?: number; apply_only_above?: number | null; audience?: string; active?: boolean; }; CreateCoinRuleDto: { /** * @description Trigger event key (lowercase alphanumeric with underscores) * @example lesson_completed */ trigger: string; /** * @description Number of coins awarded when the rule triggers * @example 10 */ amount: number; /** * @description Maximum times this rule can trigger per day * @example 5 */ daily_limit?: number; /** * @description Maximum times this rule can trigger per week * @example 20 */ weekly_limit?: number; /** * @description Maximum times this rule can trigger in total * @example 100 */ total_limit?: number; /** * @description Streak multiplier configuration (e.g. { "3": 2, "7": 3 }) * @example { * "3": 2, * "7": 3 * } */ streak_config?: Record; /** * @description Whether the rule is active * @example true */ active?: boolean; /** @description HTCH-79 weighted-random reward pool. When set, a deterministic weighted pick replaces the flat amount (Yu-kai Ch.11 Skinner Box). Pass null to clear. */ reward_pool?: Record | null; }; UpdateCoinRuleDto: { /** * @description Trigger event key (lowercase alphanumeric with underscores) * @example lesson_completed */ trigger?: string; /** * @description Number of coins awarded when the rule triggers * @example 15 */ amount?: number; /** * @description Maximum times this rule can trigger per day (null to remove) * @example 5 */ daily_limit?: number | null; /** * @description Maximum times this rule can trigger per week (null to remove) * @example 20 */ weekly_limit?: number | null; /** * @description Maximum times this rule can trigger in total (null to remove) * @example 100 */ total_limit?: number | null; /** * @description Whether the rule is active * @example true */ active?: boolean; /** * @description Streak multiplier configuration (e.g. { "3": 2, "7": 3 }) * @example { * "3": 2, * "7": 3 * } */ streak_config?: Record; /** @description HTCH-79 weighted-random reward pool. When set, a deterministic weighted pick replaces the flat amount (Yu-kai Ch.11 Skinner Box). Pass null to clear. */ reward_pool?: Record | null; }; CreateBadgeDefinitionDto: { /** * @description Unique badge key identifier * @example first_hatch */ key: string; /** * @description Audiences this badge applies to. Omit for single-audience customers. One badge_definitions row is created per audience; the badge shares its key across them. * @example [ * "student", * "teacher" * ] */ audiences?: string[]; /** * @description Human-readable badge label * @example First Hatch */ label: string; /** * @description Detailed badge description * @example Awarded for hatching your first egg */ description?: string; /** * @description HTCH-16 user-facing "How to earn" copy rendered by the badges widget. Falls back to description when empty. * @example Reach a 10-day streak by checking in every morning. */ criteria_copy?: string; /** * @description URL to the badge icon image * @example https://cdn.example.com/badges/first_hatch.png */ icon_url?: string; /** * @description Number of coins awarded when this badge is earned * @example 50 */ coin_reward: number; /** * @description Type of criteria used to determine badge eligibility * @example milestone * @enum {string} */ criteria_type: "milestone" | "streak" | "skill_level" | "collection" | "evolution" | "coin" | "custom"; /** * @description Configuration object for the criteria (schema depends on criteria_type) * @example { * "threshold": 1, * "event": "egg_hatched" * } */ criteria_config: Record; /** * @description Whether the badge should be automatically awarded when criteria are met * @example true */ auto_award?: boolean; /** * @description Whether this badge definition is active * @example true */ active?: boolean; }; GenerateBadgeIconDto: { /** * @description Badge label — drives the subject of the generated icon * @example First Hatch */ label: string; /** * @description Optional badge description for additional context * @example Awarded when a user hatches their very first buddy */ description?: string; /** * @description Visual treatment style * @default enamel_pin * @enum {string} */ style: "enamel_pin" | "medal" | "flat_modern" | "storybook" | "pixel"; /** @description Free-form hint to nudge the composition (e.g., "include a little egg with sparkles") */ hint?: string; }; UpdateBadgeDefinitionDto: { /** * @description Unique badge key identifier * @example first_hatch */ key?: string; /** * @description Replacement set of audiences for this badge. Rows are added for new audiences and removed for dropped ones (removal fails with 409 if awards exist). Omit to leave the audience set unchanged. * @example [ * "student", * "teacher" * ] */ audiences?: string[]; /** * @description Human-readable badge label * @example First Hatch */ label?: string; /** * @description Detailed badge description * @example Awarded for hatching your first egg */ description?: string; /** * @description HTCH-16 user-facing "How to earn" copy rendered by the badges widget. Pass null to clear. * @example Reach a 10-day streak by checking in every morning. */ criteria_copy?: string | null; /** * @description URL to the badge icon image * @example https://cdn.example.com/badges/first_hatch.png */ icon_url?: string; /** * @description Number of coins awarded when this badge is earned * @example 50 */ coin_reward?: number; /** * @description Type of criteria used to determine badge eligibility * @example milestone * @enum {string} */ criteria_type?: "milestone" | "streak" | "skill_level" | "collection" | "evolution" | "coin" | "custom"; /** * @description Configuration object for the criteria (schema depends on criteria_type) * @example { * "threshold": 1, * "event": "egg_hatched" * } */ criteria_config?: Record; /** * @description Whether the badge should be automatically awarded when criteria are met * @example true */ auto_award?: boolean; /** * @description Whether this badge definition is active * @example true */ active?: boolean; }; CreateEventBadgeDto: { badge_key: string; trigger_window_start: string; trigger_window_end: string; condition?: Record; narrative_callout?: Record; enabled?: boolean; }; UpdateEventBadgeDto: { badge_key?: string; trigger_window_start?: string; trigger_window_end?: string; condition?: Record; narrative_callout?: Record; enabled?: boolean; }; CreateStreakDefinitionDto: { /** * @description Audience key. Omit for single-audience customers; required if the customer has 2+ audiences configured. * @example learner */ audience?: string; /** @example daily-practice */ key: string; /** @example Daily practice streak */ label: string; description?: string; /** @enum {string} */ period: "daily" | "weekly" | "monthly"; /** * @description Events that count toward the streak (OR-matched). * @example [ * "lesson_completed", * "practice_done" * ] */ event_types: string[]; /** * @default flame * @enum {string} */ icon: "flame" | "heart" | "bolt" | "star" | "leaf"; /** * @default count * @enum {string} */ display_mode: "count" | "row" | "mini"; /** @default 7 */ max_row_icons: number; /** * @example [ * 3, * 7, * 30, * 100 * ] */ milestones?: number[]; /** @default true */ is_active: boolean; }; UpdateStreakDefinitionDto: { /** * @description Audience key. Omit for single-audience customers; required if the customer has 2+ audiences configured. * @example learner */ audience?: string; /** @example daily-practice */ key?: string; /** @example Daily practice streak */ label?: string; description?: string; /** @enum {string} */ period?: "daily" | "weekly" | "monthly"; /** * @description Events that count toward the streak (OR-matched). * @example [ * "lesson_completed", * "practice_done" * ] */ event_types?: string[]; /** * @default flame * @enum {string} */ icon: "flame" | "heart" | "bolt" | "star" | "leaf"; /** * @default count * @enum {string} */ display_mode: "count" | "row" | "mini"; /** @default 7 */ max_row_icons: number; /** * @example [ * 3, * 7, * 30, * 100 * ] */ milestones?: number[]; /** @default true */ is_active: boolean; }; IngestEventDto: { /** * @description Unique event identifier from the source system * @example evt_lesson_completed_12345 */ event_id: string; /** * @description Identifier of the user who triggered the event * @example usr_67890 */ user_id: string; /** * @description Type of the event * @example lesson_completed */ type: string; /** * @description ISO 8601 timestamp of when the event occurred. Defaults to "now" server-side if omitted. * @example 2026-04-09T12:00:00Z */ occurred_at?: string; /** * @description Audience (rol) this event belongs to. Required when the customer has 2+ audiences; omitted for single-audience customers (server uses the implicit default). * @example student */ audience?: string; /** * @description Additional properties associated with the event * @example { * "lesson_id": "lesson_42", * "score": 95, * "duration_seconds": 300 * } */ properties?: Record; }; IngestBatchDto: { /** @description Array of events to ingest (max 100) */ events: components["schemas"]["IngestEventDto"][]; }; CreateEventTypeDto: { /** * @description Machine event name. Lowercase identifiers with underscores or dots recommended. * @example lesson_completed */ name: string; /** * @description Audience key. Omit for single-audience customers. * @example learner */ audience?: string; /** * @description Human-readable label shown in the dashboard. * @example Lesson completed */ display_label?: string; /** @description Free-form description of when this event is emitted. */ description?: string; /** * @description Whether ingest accepts this event type. Defaults to true. * @default true */ is_active: boolean; }; UpdateEventTypeDto: { /** @description Rename the event. Propagates to event_ingestions, coin_rules, skill_rules, badge_definitions.condition_config.event, streak_definitions.event_types, webhook_configs.events, and custom_counters JSONB keys. */ name?: string; display_label?: string; description?: string; is_active?: boolean; }; UpsertTokenConfigDto: { /** @description Token configurations to create or update. A typical customer has exactly two entries: one primary + one progression. */ tokens: unknown[][]; }; CreateMarketplaceDto: { /** * @description Name of the marketplace * @example Avatar Shop */ name: string; /** * @description Pricing mode for marketplace items * @example mixed * @enum {string} */ pricing_mode?: "coins_only" | "free_only" | "mixed"; /** * @description How items can be unlocked * @example both * @enum {string} */ unlock_mode?: "purchase" | "earn" | "both"; /** * @description List of item categories available in this marketplace * @example [ * "head", * "background", * "accessory" * ] */ categories?: string[]; }; UpdateMarketplaceDto: { /** * @description Name of the marketplace * @example Avatar Shop */ name?: string; /** * @description Pricing mode for marketplace items * @example mixed * @enum {string} */ pricing_mode?: "coins_only" | "free_only" | "mixed"; /** * @description How items can be unlocked * @example both * @enum {string} */ unlock_mode?: "purchase" | "earn" | "both"; /** * @description List of item categories available in this marketplace * @example [ * "head", * "background", * "accessory" * ] */ categories?: string[]; /** * @description Whether the marketplace is active * @example true */ active?: boolean; }; CreateItemDto: { /** * @description Unique key identifier for the item * @example wizard-hat */ key: string; /** * @description Display label for the item * @example Wizard Hat */ label: string; /** * @description Description of the item * @example A mystical hat that grants wisdom */ description?: string; /** * @description Canonical item category. Drives compositing z-order and equip conflict detection. * @example head * @enum {string} */ category: "background" | "body" | "feet" | "hand" | "neck" | "face" | "head" | "accessory" | "booster"; /** * @description Price of the item in coins * @example 100 */ price?: number; /** * @description Rarity tier of the item * @example rare * @enum {string} */ rarity?: "common" | "uncommon" | "rare" | "epic" | "legendary"; /** * @description URL to the item image * @example https://cdn.example.com/items/wizard-hat.png */ image_url?: string; /** * @description Visibility rules controlling who can see this item * @example { * "type": "all", * "config": {} * } */ visibility_rules?: Record; /** * @description Requirements a buddy must meet to purchase this item * @example { * "min_evolution_stage": 2, * "min_total_level": 5 * } */ requirements?: Record; /** * @description Feature-specific item metadata. Booster items use booster_type, multiplier and duration_seconds. * @example { * "booster_type": "coin_x2_24h", * "multiplier": 2, * "duration_seconds": 86400 * } */ metadata?: Record; /** * @description Whether the item is active and available for purchase * @example true */ active?: boolean; /** * @description HTCH-53 Social Treasure — when true the item is gift-only: it cannot be bought for oneself, only received as a gift from a teammate. * @example false */ is_gift_only?: boolean; /** * @description HTCH-14 availability window start (ISO-8601). Null = no lower bound. * @example 2026-06-01T00:00:00.000Z */ available_from?: Record | null; /** * @description HTCH-14 availability window end (ISO-8601). Null = no upper bound. * @example 2026-07-01T00:00:00.000Z */ available_until?: Record | null; }; ImportMarketplaceItemsDto: { /** * @description Format of the import payload * @example json * @enum {string} */ format: "json" | "csv"; /** * @description Serialized import data in the specified format * @example [{"key":"sword","label":"Sword","category":"weapons","price":50}] */ payload: string; }; UpdateItemDto: { /** * @description Unique key identifier for the item * @example wizard-hat */ key?: string; /** * @description Display label for the item * @example Wizard Hat */ label?: string; /** * @description Description of the item * @example A mystical hat that grants wisdom */ description?: string; /** * @description Canonical item category. Drives compositing z-order and equip conflict detection. * @example head * @enum {string} */ category?: "background" | "body" | "feet" | "hand" | "neck" | "face" | "head" | "accessory" | "booster"; /** * @description Price of the item in coins * @example 100 */ price?: number; /** * @description Rarity tier of the item * @example rare * @enum {string} */ rarity?: "common" | "uncommon" | "rare" | "epic" | "legendary"; /** * @description URL to the item image * @example https://cdn.example.com/items/wizard-hat.png */ image_url?: string; /** * @description Visibility rules controlling who can see this item * @example { * "type": "all", * "config": {} * } */ visibility_rules?: Record; /** * @description Requirements a buddy must meet to purchase this item * @example { * "min_evolution_stage": 2, * "min_total_level": 5 * } */ requirements?: Record; /** * @description Feature-specific item metadata. Booster items use booster_type, multiplier and duration_seconds. * @example { * "booster_type": "coin_x2_24h", * "multiplier": 2, * "duration_seconds": 86400 * } */ metadata?: Record; /** * @description Whether the item is active and available for purchase * @example true */ active?: boolean; /** * @description HTCH-53 Social Treasure — when true the item is gift-only: it cannot be bought for oneself, only received as a gift from a teammate. * @example false */ is_gift_only?: boolean; /** * @description HTCH-14 availability window — ISO-8601. null clears the bound. Both nullable; null = open in that direction. * @example 2026-06-01T00:00:00.000Z */ available_from?: Record | null; /** * @description HTCH-14 availability window end. Null clears. * @example 2026-07-01T00:00:00.000Z */ available_until?: Record | null; }; ReorderItemsDto: { /** * @description Ordered list of item UUIDs defining the new display order * @example [ * "b3d7c8a0-1234-4f5e-9abc-def012345678", * "c4e8d9b1-5678-4a6f-0bcd-ef1234567890" * ] */ item_ids: string[]; }; GiftItemDto: { /** * @description UUID of the buddy that receives the gift * @example b3d7c8a0-1234-4f5e-9abc-def012345678 */ to_buddy_id: string; /** * @description Optional note delivered with the gift (max 280 chars) * @example Great work on the launch — wear it with pride! */ message?: string; }; ScheduleFlashSaleDto: { name: string; starts_at: string; duration_minutes?: number; discount_percent?: number; item_selection_mode?: string; curated_item_ids?: string[]; }; MarketplaceFomoQueryDto: { item_ids: string[]; }; EquipLegacyItemDto: Record; CreateWebhookConfigDto: { /** * @description URL that will receive webhook POST requests * @example https://api.example.com/webhooks/hatched */ url: string; /** * @description List of event types to subscribe to. If omitted, subscribes to all events. * @example [ * "buddy.hatched", * "coins.earned", * "badge.awarded" * ] */ events?: string[]; }; WebhookEventTypesResponseDto: { /** * @description Canonical event types accepted by webhook subscriptions. * @example [ * "egg.created", * "buddy.hatched", * "badge.awarded" * ] */ events: string[]; }; UpdateWebhookConfigDto: { /** * @description URL that will receive webhook POST requests * @example https://api.example.com/webhooks/hatched */ url?: string; /** * @description List of event types to subscribe to * @example [ * "buddy.hatched", * "coins.earned", * "badge.awarded" * ] */ events?: string[]; /** * @description Whether the webhook config is active * @example true */ active?: boolean; }; FeatureActivityWeekDto: { /** * @description ISO week start date. * @example 2026-06-01 */ week_start: string; /** * @description Events in that week. * @example 12 */ count: number; }; FeatureActivityFeatureDto: { /** * @description Planner feature key. * @example kudos */ feature_key: string; /** * @description Display label. * @example Kudos */ label: string; /** * @description Telemetry event types rolled up under this feature. * @example [ * "kudos.sent" * ] */ event_types: string[]; /** * @description Total events in the window. * @example 84 */ total: number; /** * @description Total events in the preceding window (delta baseline). * @example 61 */ prev_total: number; weekly: components["schemas"]["FeatureActivityWeekDto"][]; }; FeatureActivityResponseDto: { /** * @description Lookback window in weeks. * @example 8 */ weeks: number; features: components["schemas"]["FeatureActivityFeatureDto"][]; }; BillingCreditsDto: { /** @example 20 */ welcome: number; /** @example 600 */ paid: number; /** @example 50 */ promo: number; /** * Format: date-time * @description Promo credit expiry, or null when there are no expiring promo credits. */ promo_expires_at?: string | null; /** @example 670 */ total_spendable: number; }; BillingEventQuotaDto: { /** * @description Monthly event limit. Null means unlimited. * @example 500000 */ limit: Record | null; /** @example 120450 */ used: number; /** Format: date-time */ reset_at: string; }; BillingIncludedCreditsDto: { /** * @description Monthly included AI credit grant. Null means unlimited. * @example 50 */ monthly: Record | null; /** * @description Included AI credit grant for the current Stripe billing period. Null means unlimited or unknown. * @example 600 */ current_period: Record | null; /** * @description Detected Stripe billing interval for the current subscription, if known. * @enum {string|null} */ interval: "monthly" | "annual" | null; }; BillingSubscriptionDto: { /** * @description Current Stripe subscription id, if the customer has one. * @example sub_123 */ stripe_subscription_id?: Record | null; /** * @description Live Stripe subscription status when it could be fetched. * @example active */ status?: Record | null; /** * Format: date-time * @description Current Stripe period end. For active subscriptions this is the next invoice date. */ current_period_end_at?: string | null; /** * Format: date-time * @description Next invoice date for the current subscription, when known. */ next_invoice_at?: string | null; /** @example false */ cancel_at_period_end: boolean; /** * @description Detected Stripe billing interval for the current subscription, if known. * @enum {string|null} */ interval: "monthly" | "annual" | null; /** * @description True when the subscription summary was refreshed from Stripe during this request. * @example true */ synced: boolean; }; BillingStatusResponseDto: { /** @enum {string} */ plan: "starter" | "growth" | "pro" | "enterprise"; /** @example Launch */ plan_public_name: string; /** @example active */ billing_status: string; /** @example cus_123 */ stripe_customer_id?: Record | null; /** @example true */ has_subscription: boolean; credits: components["schemas"]["BillingCreditsDto"]; event_quota: components["schemas"]["BillingEventQuotaDto"]; included_credits: components["schemas"]["BillingIncludedCreditsDto"]; subscription: components["schemas"]["BillingSubscriptionDto"]; /** @description Raw plan definition from PLAN_MATRIX retained for backwards compatibility. */ plan_matrix: Record; }; SeedFromUrlDto: { /** * @description Operator's public website URL to scrape for onboarding seed * @example https://flalingo.com */ url: string; }; RepoIntegrationContextDto: Record; RepoBriefDto: { product_description: string; audience: string; sector?: string; gamification_goals: string[]; possible_events: string[]; feature_fit_scores: Record; integration_context?: components["schemas"]["RepoIntegrationContextDto"]; confidence?: Record; notes?: string; }; SeedFromRepoDto: { /** @description Brand brief JSON produced by the operator's AI coding assistant after running the Hatched repo-analysis prompt. */ brief: components["schemas"]["RepoBriefDto"]; /** @description Which agent produced the brief (claude-code / codex / copilot / other). */ agent?: string; }; SeedFromDescriptionDto: { /** @enum {string} */ audience: "employees" | "customers" | "learners" | "players" | "members" | "mixed"; /** @enum {string} */ surface: "web" | "mobile" | "intranet" | "offline" | "internal_tool"; /** @enum {string} */ org_type: "hr" | "ld" | "sales" | "customer_success" | "community" | "other"; /** @description Optional free-text the operator adds after the chips. */ description?: string; }; WaitlistDto: { /** * @description Integration kind the operator wants to be notified about * @example github * @enum {string} */ kind: "github" | "mixpanel"; /** * @description Email to notify * @example pm@example.com */ email: string; /** @description Optional free-text note from the operator */ notes?: string; }; ProductSummary: Record; PriorityProfile: Record; UpdateAnswersDto: { product?: components["schemas"]["ProductSummary"]; goals?: string[]; events?: string[]; marketplace_enabled?: boolean; evolution_enabled?: boolean; badges_enabled?: boolean; tokens_enabled?: boolean; streaks_enabled?: boolean; paths_enabled?: boolean; skill_names_approved?: string[]; badge_names_approved?: string[]; item_categories_enabled?: string[]; priority_profile?: components["schemas"]["PriorityProfile"]; feature_recommendations?: Record; }; SendMessageDto: { content: string; }; CreateUploadUrlDto: { stage: number; mime_type: string; byte_size: number; }; CommitAssetDto: { storage_path: string; }; UpsertTokenGateDto: { /** * @description Stable gate identifier. snake_case, under 100 chars. Unique per customer. * @example advanced-mode */ gate_key: string; /** * @description Token key this gate consumes (must match a token_config row). * @example gems */ token_key: string; /** * @description Token cost to unlock this gate. * @example 50 */ cost: number; /** @description Display label shown in dashboard/widgets. */ label?: string; /** @description Free-form description of what unlocking grants. */ description?: string; /** * @description Arbitrary metadata the app code reads to decide what the unlock enables. * @example { * "feature": "advanced-mode", * "min_evolution_stage": 2 * } */ metadata?: Record; /** @default true */ is_active: boolean; }; CreatePathDefinitionDto: { /** * @description Audience key. Omit for single-audience customers; required if 2+ audiences are configured. * @example learner */ audience?: string; /** @example english_a2 */ key: string; /** @example English A2 path */ label: string; description?: string; /** * @default path * @enum {string} */ icon: "path" | "flame" | "heart" | "bolt" | "star" | "leaf"; /** @description Hex accent color override (e.g. "#7c3aed"). Falls back to brand accent. */ accent_color?: string; /** * @default straight * @enum {string} */ display_mode: "straight" | "zigzag" | "stepper"; /** @default true */ is_active: boolean; }; UpdatePathDefinitionDto: { /** * @description Audience key. Omit for single-audience customers; required if 2+ audiences are configured. * @example learner */ audience?: string; /** @example english_a2 */ key?: string; /** @example English A2 path */ label?: string; description?: string; /** * @default path * @enum {string} */ icon: "path" | "flame" | "heart" | "bolt" | "star" | "leaf"; /** @description Hex accent color override (e.g. "#7c3aed"). Falls back to brand accent. */ accent_color?: string; /** * @default straight * @enum {string} */ display_mode: "straight" | "zigzag" | "stepper"; /** @default true */ is_active: boolean; }; CreatePathStepDto: { /** @example speaking_sprint */ key: string; /** @example Speaking sprint */ label: string; description?: string; icon?: string; /** @example 1 */ ordinal: number; /** * @description Optional override for unlock semantics. If omitted, the step unlocks when the previous step (by ordinal) is completed. * @example { * "type": "badge_earned", * "config": { * "badge_key": "novice" * } * } */ unlock_condition?: Record; /** * @description Optional step-level completion shortcut. When set and matched by the rule engine, the step completes outright and any incomplete sub-steps cascade to completed. Cannot be `custom` (use sub-step manual completion for that flow). * @example { * "type": "event_count", * "config": { * "event_type": "lesson_completed", * "threshold": 5 * } * } */ completion_condition?: Record; /** @default 0 */ reward_coins: number; reward_badge_key?: string; /** @default true */ is_active: boolean; }; ReorderEntryDto: { /** @description UUID of the step or sub-step to place at this ordinal */ id: string; ordinal: number; }; ReorderDto: { ordering: components["schemas"]["ReorderEntryDto"][]; }; UpdatePathStepDto: { /** @example speaking_sprint */ key?: string; /** @example Speaking sprint */ label?: string; description?: string; icon?: string; /** @example 1 */ ordinal?: number; /** * @description Optional override for unlock semantics. If omitted, the step unlocks when the previous step (by ordinal) is completed. * @example { * "type": "badge_earned", * "config": { * "badge_key": "novice" * } * } */ unlock_condition?: Record; /** * @description Optional step-level completion shortcut. When set and matched by the rule engine, the step completes outright and any incomplete sub-steps cascade to completed. Cannot be `custom` (use sub-step manual completion for that flow). * @example { * "type": "event_count", * "config": { * "event_type": "lesson_completed", * "threshold": 5 * } * } */ completion_condition?: Record; /** @default 0 */ reward_coins: number; reward_badge_key?: string; /** @default true */ is_active: boolean; }; CreatePathSubStepDto: { /** @example lesson_3 */ key: string; /** @example Greetings */ label: string; description?: string; /** @example 1 */ ordinal: number; /** * @description Rule-engine predicate evaluated after each event. Omit and set allow_manual_complete=true for purely manual sub-steps. * @example { * "type": "event_count", * "config": { * "event_type": "lesson_completed", * "threshold": 3 * } * } */ completion_condition?: Record; /** @default false */ allow_manual_complete: boolean; /** * @description When true, sub-steps inside the same step can be completed out of ordinal order. * @default false */ allow_skip_ahead: boolean; /** @default 0 */ reward_coins: number; reward_badge_key?: string; /** @description Optional deep-link to the customer LMS/app where the sub-step lives. The widget renders a CTA button when present. */ content_url?: string; /** @example Open lesson */ cta_label?: string; /** @default true */ is_active: boolean; }; UpdatePathSubStepDto: { /** @example lesson_3 */ key?: string; /** @example Greetings */ label?: string; description?: string; /** @example 1 */ ordinal?: number; /** * @description Rule-engine predicate evaluated after each event. Omit and set allow_manual_complete=true for purely manual sub-steps. * @example { * "type": "event_count", * "config": { * "event_type": "lesson_completed", * "threshold": 3 * } * } */ completion_condition?: Record; /** @default false */ allow_manual_complete: boolean; /** * @description When true, sub-steps inside the same step can be completed out of ordinal order. * @default false */ allow_skip_ahead: boolean; /** @default 0 */ reward_coins: number; reward_badge_key?: string; /** @description Optional deep-link to the customer LMS/app where the sub-step lives. The widget renders a CTA button when present. */ content_url?: string; /** @example Open lesson */ cta_label?: string; /** @default true */ is_active: boolean; }; WinStateBragResponseDto: { /** @description BragPayload — share copy, public profile URL, and OG-card refs the ceremony Share CTA renders. */ payload: Record; /** * @description Tenant-enabled share channels; empty when the Planner toggle is off (the BragButton self-hides). * @example [ * "linkedin", * "copy_link" * ] */ enabled_channels: string[]; }; CreateSurpriseDropDto: { name: string; trigger_event: string; chance: number; reward_type: string; reward_amount?: number; reward_ref?: string; condition?: Record; narrative_copy: Record; enabled?: boolean; }; UpdateSurpriseDropDto: { name?: string; trigger_event?: string; chance?: number; reward_type?: string; reward_amount?: number; reward_ref?: string; condition?: Record; narrative_copy?: Record; enabled?: boolean; }; AdminGrantBoosterDto: { buddy_id: string; key: string; }; PurchaseBoosterDto: { key: string; }; CreateLotteryDto: { name: string; eligibility_rule: Record; cadence: string; prize_pool: string[]; draw_at: Record; tz?: string; active?: boolean; }; UpdateLotteryDto: { name?: string; eligibility_rule?: Record; cadence?: string; prize_pool?: string[]; draw_at?: Record; tz?: string; active?: boolean; }; CreateProfileTemplateDto: { name: string; source_template: string; layout: Record; theme_overrides?: Record; is_default?: boolean; }; UpdateProfileTemplateDto: { name?: string; layout?: Record; theme_overrides?: Record; is_default?: boolean; }; BulkApplyTargetDto: Record; BulkApplyDto: { template_id: string; target: components["schemas"]["BulkApplyTargetDto"]; }; SeasonClosingDiscoveryResponseDto: { /** @description SeasonHighlightsSnapshot — the four personalized highlights plus cohort outcome; `available: false` when the buddy has no closed season. */ snapshot: Record; /** @description Closed season UUID, or null when none exists. */ season_id: string | null; /** @description Name of the tier below the buddy’s final tier (demotion-grief copy), or null. */ lower_tier_name: string | null; }; StartScoutingQuestDto: { prediction_value: number; }; ReplaceLeagueTiersDto: { tiers: string[]; }; BossFightConfigDto: { enabled: boolean; name: string; description: string; target_metric: string; target_value: number; badge_key?: Record; mythic_item_id?: Record; ghost_grave_enabled: boolean; }; UpdateLeagueConfigDto: { season_duration_days?: number; start_day_of_week?: number; start_hour?: number; cohort_size?: number; promote_pct?: number; demote_pct?: number; off_season_duration_days?: number; off_season_mystery_box_multiplier?: number; off_season_wardrobe_drop_ids?: string[]; off_season_scouting_quest_enabled?: boolean; boss_fight?: components["schemas"]["BossFightConfigDto"]; auto_renew?: boolean; promotion_reward_coins?: number; }; ScheduleLeagueSeasonDto: { name?: string; starts_at?: string; }; CreateShowroomDto: Record; UpdateShowroomDto: Record; AwardDto: Record; CreateSurfaceDto: Record; SurfaceWidgetReadinessEntryDto: { /** * @description Widget mount key from the surface layout. * @example streak */ key: string; /** * @description False when the public layout resolver will hide this widget from players. * @example false */ servable: boolean; /** * @description Why the widget is (un)servable. * @example no_active_streak * @enum {string} */ reason: "ready" | "no_active_streak" | "no_marketplace_items" | "no_badge_definitions"; /** * @description Resolved tenant streak key injected into the streak mount (streak widgets only). * @example daily_lesson */ streak_key?: string; }; SurfaceWidgetReadinessResponseDto: { entries: components["schemas"]["SurfaceWidgetReadinessEntryDto"][]; }; UpdateSurfaceDto: Record; CreatePlayerDto: Record; UpdatePlayerDto: Record; UpsertRecipeDto: Record; RunRecipeDto: Record; StartSessionDto: Record; }; responses: never; parameters: never; requestBodies: never; headers: never; pathItems: never; }; ``` ### `$defs` ```ts export type $defs = Record; ``` ### `operations` ```ts export interface operations { CustomersController_getProfile: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CustomersController_updateProfile: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UpdateCustomerDto"]; }; }; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CustomersController_getReferral: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CustomersController_updateSettings: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UpdateSettingsDto"]; }; }; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CustomersController_suggestWidgetTheme: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CustomersController_updateAudiences: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UpdateAudiencesDto"]; }; }; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CustomersController_regenerateAssets: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CustomersController_deleteUserData: { parameters: { query?: never; header?: never; path: { user_id: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CustomerFeatureToggleController_getState: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CustomerFeatureToggleController_updateToggles: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CustomerFeatureToggleController_getOctalysisState: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CustomerFeatureToggleController_publishDraft: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CustomerFeatureToggleController_discardDraft: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CustomerFeatureToggleController_getFeatureConfig: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CustomerFeatureToggleController_updateFeatureConfig: { parameters: { query?: never; header?: never; path: { feature_key: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CustomerNarrativeController_getNarrative: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CustomerNarrativeController_updateNarrative: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CustomerNarrativeController_getNarrativeAudit: { parameters: { query: { field: string; limit: string; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CustomerMissionAnchorController_getConfig: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CustomerMissionAnchorController_updateConfig: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; SixDWizardController_get: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; SixDWizardController_apply: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; SixDWizardController_patch: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; SixDWizardController_skip: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; SixDWizardController_audit: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; SixDWizardController_driftStats: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; AuthController_register: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["RegisterCustomerDto"]; }; }; responses: { /** @description Customer registered successfully. Returns a dashboard JWT; raw API keys are NOT returned here — call POST /auth/api-keys after login to mint one. */ 201: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Validation error or email already in use */ 400: { headers: { [name: string]: unknown; }; content?: never; }; }; }; AuthController_login: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["LoginDto"]; }; }; responses: { /** @description Login successful, returns customer data and JWT token */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Invalid email or password */ 401: { headers: { [name: string]: unknown; }; content?: never; }; }; }; AuthController_getSsoConfig: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; AuthController_startSso: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; AuthController_completeSso: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; AuthController_changePassword: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["ChangePasswordDto"]; }; }; responses: { /** @description Password changed */ 200: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["PasswordUpdatedResponseDto"]; }; }; }; }; AuthController_requestPasswordReset: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["RequestPasswordResetDto"]; }; }; responses: { /** @description Always returns the same message regardless of whether the account exists (no account enumeration). reset_url/expires_at are only present for local dashboard origins. */ 200: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["PasswordResetRequestedResponseDto"]; }; }; }; }; AuthController_resetPassword: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["ResetPasswordDto"]; }; }; responses: { /** @description Password reset */ 200: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["PasswordUpdatedResponseDto"]; }; }; }; }; AuthController_verifyEmail: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["VerifyEmailDto"]; }; }; responses: { /** @description Email verified */ 200: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["EmailVerificationResponseDto"]; }; }; }; }; AuthController_requestEmailVerification: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Email verification requested */ 200: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["EmailVerificationRequestedResponseDto"]; }; }; }; }; AuthController_me: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Returns the authenticated customer profile and settings */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized - invalid or missing JWT token */ 401: { headers: { [name: string]: unknown; }; content?: never; }; }; }; AuthController_whoami: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Caller identity + plan capabilities resolved. */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized — credential is missing, expired, or revoked */ 401: { headers: { [name: string]: unknown; }; content?: never; }; }; }; AuthController_listApiKeys: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Returns a list of active API keys with masked key values */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; }; }; AuthController_createApiKey: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["CreateApiKeyDto"]; }; }; responses: { /** @description API key created successfully. The raw key is returned only once. */ 201: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Maximum number of API keys reached */ 400: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; }; }; AuthController_rotateApiKey: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["CreateApiKeyDto"]; }; }; responses: { /** @description All previous keys revoked and a new key created */ 201: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; }; }; AuthController_revokeApiKey: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description API key revoked successfully */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description API key not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; AuthController_createPublishableKey: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Publishable key created. The raw key is returned only once. */ 201: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Scope set rejected */ 400: { headers: { [name: string]: unknown; }; content?: never; }; }; }; OperationsController_findOne: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Returns the operation details */ 200: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["OperationResponseDto"]; }; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Operation not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; OperationsController_findAll: { parameters: { query: { type: string; status: string; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Returns an array of operations */ 200: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["OperationResponseDto"][]; }; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; }; }; OperationsController_cancel: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Operation cancelled successfully */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Operation cannot be cancelled in its current state */ 400: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Operation not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WidgetSessionsController_createPreviewTokens: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WidgetSessionsController_getPlayerZeroStatus: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Player Zero existence + hatch status */ 200: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["PlayerZeroStatusResponseDto"]; }; }; }; }; WidgetSessionsController_createPlayerZero: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Player Zero created or already present */ 201: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["PlayerZeroResponseDto"]; }; }; /** @description No published config version to attach the player to */ 400: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WidgetSessionsController_createEmbedToken: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["CreateEmbedTokenDto"]; }; }; responses: { /** @description Embed token created */ 201: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["CreateEmbedTokenResponseDto"]; }; }; /** @description Buddy does not exist for this tenant */ 404: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Validation failed */ 422: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WidgetSessionsController_createSessionToken: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["CreateSessionTokenDto"]; }; }; responses: { /** @description Session token created with scoped permissions */ 201: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["CreateSessionTokenResponseDto"]; }; }; /** @description Buddy does not exist for this tenant */ 404: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Validation failed */ 422: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WidgetSessionsController_revokeSession: { parameters: { query?: never; header?: never; path: { /** @description Widget session UUID */ id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Session revoked successfully */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Widget session not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WidgetSessionsController_verifyInstallation: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["VerifyInstallationDto"]; }; }; responses: { /** @description Verification result */ 200: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["VerifyInstallationResponseDto"]; }; }; /** @description No URL supplied and none configured */ 400: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WidgetApiController_getBuddy: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Buddy state and widget config returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WidgetApiController_createShareLink: { parameters: { query: { channel: string; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Share link payload */ 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WidgetApiController_recordShareEvent: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["RecordShareEventDto"]; }; }; responses: { /** @description Recorded (best-effort) */ 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WidgetApiController_markBuddyHatched: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["WidgetBuddyHatchedDto"]; }; }; responses: { /** @description Recorded (idempotent) */ 201: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["BuddyHatchedResponseDto"]; }; }; }; }; WidgetApiController_updateBuddySeo: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["WidgetBuddySeoDto"]; }; }; responses: { /** @description Updated SEO preference */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WidgetApiController_updateBuddyProfile: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["WidgetBuddyProfileDto"]; }; }; responses: { /** @description Updated profile preferences */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WidgetApiController_getState: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Aggregate state returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Not modified (ETag matched) */ 304: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WidgetApiController_getNarrative: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Resolved narrative returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WidgetApiController_getNarrativeArc: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Program Chapters arc view returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WidgetApiController_getMissionAnchor: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Mission Anchor payload returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WidgetApiController_recordRendered: { parameters: { query?: never; header: { origin: string; }; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["WidgetRenderedDto"]; }; }; responses: { /** @description Render beacon accepted */ 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WidgetApiController_trackEvent: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["TrackEventDto"]; }; }; responses: { /** @description Event accepted; effects returned */ 201: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Invalid event or type not registered */ 400: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WidgetApiController_getMarketplace: { parameters: { query?: { /** @description Filter by item category */ category?: string; /** @description Filter by rarity tier */ rarity?: string; /** @description Page number (default: 1) */ page?: number; /** @description Items per page (default: 50) */ limit?: number; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Paginated marketplace items with visibility and ownership info */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WidgetApiController_trackMarketplaceItemView: { parameters: { query?: never; header?: never; path: { /** @description Item UUID */ id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Impression recorded */ 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WidgetApiController_purchaseItem: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["PurchaseItemDto"]; }; }; responses: { /** @description Item purchased successfully */ 201: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Insufficient coins or item not available */ 400: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Missing required scope */ 403: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WidgetApiController_equipItems: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["EquipItemsDto"]; }; }; responses: { /** @description Items equipped/unequipped successfully */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Missing required scope */ 403: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WidgetApiController_rerenderAppearance: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Rerender queued */ 202: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Buddy is already ready or has another op in flight */ 409: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WidgetApiController_previewOutfit: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["PreviewOutfitDto"]; }; }; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WidgetApiController_compositionStatus: { parameters: { query: { variant_id: string; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WidgetApiController_listOutfits: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WidgetApiController_saveOutfit: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["SaveOutfitDto"]; }; }; responses: { /** @description Outfit saved */ 201: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Outfit cap reached */ 409: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WidgetApiController_activateOutfit: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WidgetApiController_deleteOutfit: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WidgetApiController_getBadges: { parameters: { query?: { /** @description Include locked badge definitions in the legacy locked array (default true) */ include_locked?: boolean; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Earned + locked badges returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WidgetApiController_getActivePath: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Path runtime payload, or null when no active path exists */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WidgetApiController_getPathByKey: { parameters: { query?: never; header?: never; path: { /** @description Path definition key (URL-safe slug) */ key: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Path runtime payload returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Path not found for this audience */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WidgetApiController_completePathSubStep: { parameters: { query?: never; header?: never; path: { /** @description Path definition key */ key: string; /** @description Sub-step key */ subKey: string; }; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WidgetApiController_getStreak: { parameters: { query?: never; header?: never; path: { /** @description Streak definition key (URL-safe slug) */ key: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Streak definition + current progress */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Streak not found or inactive */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WidgetApiController_getEvolutions: { parameters: { query?: { /** @description Max rows (default 10, max 50) */ limit?: number; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Paginated evolution history */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WidgetApiController_getTokens: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Token wallet returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WidgetApiController_getOperation: { parameters: { query?: never; header?: never; path: { /** @description Operation UUID */ id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Operation status returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Operation not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WidgetApiController_getLiveTheme: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Live theme returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WidgetApiController_getNextBestAction: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Next-best-action returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; PresetsController_listPresets: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description List of available presets returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; PresetsController_getPreset: { parameters: { query?: never; header?: never; path: { /** @description Preset key identifier */ key: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Preset configuration returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Preset not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; PresetsController_applyPreset: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["ApplyPresetDto"]; }; }; responses: { /** @description Preset applied successfully */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Invalid preset key */ 400: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Preset not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; ConfigVersionsController_findAll: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description List of config versions returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; ConfigVersionsController_create: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["CreateConfigVersionDto"]; }; }; responses: { /** @description Draft config version created or returned */ 201: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Invalid input */ 400: { headers: { [name: string]: unknown; }; content?: never; }; }; }; ConfigVersionsController_findById: { parameters: { query?: never; header?: never; path: { /** @description Config version UUID */ id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Config version returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Config version not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; ConfigVersionsController_update: { parameters: { query?: never; header?: never; path: { /** @description Config version UUID */ id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UpdateConfigVersionDto"]; }; }; responses: { /** @description Config version updated */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Config version not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; ConfigVersionsController_publish: { parameters: { query?: never; header?: never; path: { /** @description Config version UUID */ id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Config version published */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Draft has no configuration to publish */ 400: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Config version not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Config version is not a draft (already published or archived) and cannot be published */ 409: { headers: { [name: string]: unknown; }; content?: never; }; }; }; ConfigVersionsController_getImpact: { parameters: { query?: never; header?: never; path: { /** @description Config version UUID */ id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Impact analysis returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Config version not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; ConfigVersionsController_clone: { parameters: { query?: never; header?: never; path: { /** @description Source config version UUID to clone from */ id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description New draft config version created from clone */ 201: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Source config version not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; ConfigVersionsController_migrateBuddies: { parameters: { query?: never; header?: never; path: { /** @description Published config version UUID */ id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Buddies migrated successfully */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Config version not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; EggsController_findAll: { parameters: { query: { user_id: string; status: string; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Returns an array of eggs matching the filters */ 200: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["EggResponseDto"][]; }; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; }; }; EggsController_create: { parameters: { query?: { /** @description When true, return the user's most recent waiting/ready egg if one exists instead of creating a new one (idempotent first-run bootstrap). */ ensure?: boolean; }; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["CreateEggDto"]; }; }; responses: { /** @description Egg created (or existing active egg returned when ensure=true) */ 201: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["EggResponseDto"]; }; }; /** @description Validation error */ 400: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description No published config version (no_published_config), or the per-user active-egg limit was reached (active_egg_limit) */ 409: { headers: { [name: string]: unknown; }; content?: never; }; }; }; EggsController_findById: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Returns the egg details */ 200: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["EggResponseDto"]; }; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Egg not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; EggsController_updateStatus: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UpdateEggStatusDto"]; }; }; responses: { /** @description Egg status updated successfully */ 200: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["EggStatusChangeResponseDto"]; }; }; /** @description Invalid status transition */ 400: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Egg not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; EggsController_hatch: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Hatch process initiated, returns an operation ID to poll for status */ 202: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Egg is not in a hatchable state */ 400: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Egg not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BuddiesController_findAll: { parameters: { query?: { /** @description Page number (1-based) */ page?: number; /** @description Number of items per page */ limit?: number; /** @description Field name to sort by */ sort?: string; /** @description Sort direction */ order?: "asc" | "desc"; /** @description Filter buddies by user identifier */ user_id?: string; /** @description Free-text search across buddy name and user_id (case-insensitive substring). */ search?: string; /** @description Filter buddies by status */ status?: "active" | "archived"; /** @description Filter buddies by evolution stage */ evolution_stage?: number; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Returns a paginated list of buddies */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BuddiesController_getUserSummary: { parameters: { query?: never; header?: never; path: { user_id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Returns the aggregated user summary */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BuddiesController_findById: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Returns the buddy with skills, items, badges, and evolution info */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Buddy not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BuddiesController_update: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UpdateBuddyDto"]; }; }; responses: { /** @description Buddy name updated successfully */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Name is required */ 400: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Buddy not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BuddiesController_archive: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Buddy archived successfully */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Buddy is already archived */ 400: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Buddy not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BuddiesController_updateSkills: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UpdateBuddySkillsDto"]; }; }; responses: { /** @description Skill levels updated successfully */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Validation error in skill updates */ 400: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Buddy not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BuddiesController_earnCoins: { parameters: { query?: never; header: { "idempotency-key": string; }; path: { buddy_id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["EarnCoinsDto"]; }; }; responses: { /** @description Coins earned successfully, returns updated balance */ 201: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Buddy not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BuddiesController_spendCoins: { parameters: { query?: never; header: { "idempotency-key": string; }; path: { buddy_id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["SpendCoinsDto"]; }; }; responses: { /** @description Coins spent successfully, returns updated balance */ 201: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Insufficient coin balance */ 400: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Buddy not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BuddiesController_getBuddyBadges: { parameters: { query?: never; header?: never; path: { buddy_id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Returns the list of badge awards */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Buddy not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BuddiesController_awardBadge: { parameters: { query?: never; header?: never; path: { buddy_id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["AwardBadgeDto"]; }; }; responses: { /** @description Badge awarded successfully */ 201: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Badge already awarded or invalid badge key */ 400: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Buddy not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BuddiesController_getEvolutionReadiness: { parameters: { query?: never; header?: never; path: { buddy_id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Returns the evolution status and progress toward the next stage */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Buddy not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BuddiesController_evolve: { parameters: { query?: never; header?: never; path: { buddy_id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Evolution process initiated, returns an operation ID to poll */ 202: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Buddy does not meet evolution requirements */ 400: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Buddy not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BuddiesController_purchaseItem: { parameters: { query?: never; header: { "idempotency-key": string; }; path: { buddy_id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["PurchaseItemDto"]; }; }; responses: { /** @description Item purchased successfully */ 201: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Insufficient coins or item already owned */ 400: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Buddy or item not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BuddiesController_unlockItem: { parameters: { query?: never; header?: never; path: { buddy_id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UnlockItemDto"]; }; }; responses: { /** @description Item unlocked successfully */ 201: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Item already unlocked */ 400: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Buddy or item not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BuddiesController_equipItems: { parameters: { query?: never; header?: never; path: { buddy_id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["EquipItemsDto"]; }; }; responses: { /** @description Items equipped/unequipped successfully */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Item not owned or invalid item ID */ 400: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Buddy not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BuddiesController_rerenderAppearance: { parameters: { query?: never; header?: never; path: { buddy_id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Rerender queued; returns operation_id */ 201: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Buddy not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Buddy already ready or another op in flight */ 409: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BuddiesController_getPurchasedItems: { parameters: { query?: never; header?: never; path: { buddy_id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Returns the list of purchased items */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Buddy not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BuddiesController_getTokens: { parameters: { query?: never; header?: never; path: { buddy_id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Typed token summary */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BuddiesController_tokenTransaction: { parameters: { query?: never; header: { "idempotency-key": string; }; path: { buddy_id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["TokenTransactionDto"]; }; }; responses: { /** @description Token transaction completed successfully */ 201: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Insufficient token balance or invalid token type */ 400: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Buddy not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BuddiesController_listEvolutions: { parameters: { query: { page: string; limit: string; }; header?: never; path: { buddy_id: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BuddiesController_getProgressionLegacy: { parameters: { query?: never; header?: never; path: { buddy_id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Returns progression metrics for the buddy */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Buddy not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BuddiesController_getProgression: { parameters: { query?: never; header?: never; path: { buddy_id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Returns detailed progression metrics for the buddy */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Buddy not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CustomersSummaryController_getUserSummary: { parameters: { query?: never; header?: never; path: { user_id: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BuddyShareController_createShare: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["CreateShareDto"]; }; }; responses: { /** @description { enabled, share_code, share_url, card_url } */ 201: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Buddy not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BuddyShareController_recordEvent: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["ShareEventDto"]; }; }; responses: { /** @description Recorded (best-effort) */ 204: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BuddyShareStatsController_getStats: { parameters: { query: { window_days: number; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Share funnel aggregate */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BuddyShareStatsController_getSettings: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Resolved + raw sharing settings */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BuddyShareStatsController_updateSettings: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UpdateSharingDto"]; }; }; responses: { /** @description Updated sharing settings */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; PublicShareController_getPublicBuddy: { parameters: { query?: never; header?: never; path: { code: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Public buddy view */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unknown code or sharing disabled */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; PublicShareController_recordEvent: { parameters: { query?: never; header?: never; path: { code: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["PublicShareEventDto"]; }; }; responses: { /** @description Recorded (best-effort) */ 204: { headers: { [name: string]: unknown; }; content?: never; }; }; }; PublicShareIndexController_getShareIndex: { parameters: { query: { offset: number; limit: number; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Paged share-code index */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; ProfilePageController_getProfile: { parameters: { query?: never; header?: never; path: { code: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Profile page view */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unknown code, sharing disabled, or capability not entitled */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; SkillSetsController_findAll: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description List of skill sets returned successfully */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; }; }; SkillSetsController_create: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["CreateSkillSetDto"]; }; }; responses: { /** @description Skill set created successfully */ 201: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Validation error */ 400: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; }; }; SkillSetsController_generateIcon: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["GenerateSkillIconDto"]; }; }; responses: { /** @description Skill icon generated successfully */ 201: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Validation error */ 400: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Image generation budget exceeded */ 402: { headers: { [name: string]: unknown; }; content?: never; }; }; }; SkillSetsController_findById: { parameters: { query?: never; header?: never; path: { /** @description UUID of the skill set */ id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Skill set returned successfully */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Skill set not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; SkillSetsController_update: { parameters: { query?: never; header?: never; path: { /** @description UUID of the skill set */ id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UpdateSkillSetDto"]; }; }; responses: { /** @description Skill set updated successfully */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Validation error */ 400: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Skill set not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; SkillSetsController_delete: { parameters: { query?: never; header?: never; path: { /** @description UUID of the skill set */ id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Skill set deleted successfully */ 204: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Skill set not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Cannot delete skill set in use by buddies */ 409: { headers: { [name: string]: unknown; }; content?: never; }; }; }; SkillRulesController_findAll: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description List of skill rules returned successfully */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; }; }; SkillRulesController_create: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["CreateSkillRuleDto"]; }; }; responses: { /** @description Skill rule created successfully */ 201: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Validation error */ 400: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Skill rule with this trigger and skill_key already exists */ 409: { headers: { [name: string]: unknown; }; content?: never; }; }; }; SkillRulesController_update: { parameters: { query?: never; header?: never; path: { /** @description UUID of the skill rule */ id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UpdateSkillRuleDto"]; }; }; responses: { /** @description Skill rule updated successfully */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Validation error */ 400: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Skill rule not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; SkillRulesController_delete: { parameters: { query?: never; header?: never; path: { /** @description UUID of the skill rule */ id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Skill rule deleted successfully */ 204: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Skill rule not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; SkillRulesController_applyThemePack: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Pack applied */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Template key not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; SkillDecayRulesController_findAll: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description List returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; SkillDecayRulesController_create: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["CreateSkillDecayRuleDto"]; }; }; responses: { /** @description Skill decay rule created */ 201: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Validation error */ 400: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; }; }; SkillDecayRulesController_preview: { parameters: { query: { /** @description Starting skill level */ start: number; /** @description How many cadence periods to project */ periods: number; }; header?: never; path: { /** @description UUID of the skill decay rule */ id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Preview computed */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; SkillDecayRulesController_update: { parameters: { query?: never; header?: never; path: { /** @description UUID of the skill decay rule */ id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UpdateSkillDecayRuleDto"]; }; }; responses: { /** @description Updated */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; SkillDecayRulesController_delete: { parameters: { query?: never; header?: never; path: { /** @description UUID of the skill decay rule */ id: string; }; cookie?: never; }; requestBody?: never; responses: { 204: { headers: { [name: string]: unknown; }; content?: never; }; }; }; SkillDecayRulesController_history: { parameters: { query?: never; header?: never; path: { /** @description UUID of the skill decay rule */ id: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; SkillDecayRulesController_runNow: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Sweep enqueued */ 202: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CoinRulesController_findAll: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description List of coin rules returned successfully */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CoinRulesController_create: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["CreateCoinRuleDto"]; }; }; responses: { /** @description Coin rule created successfully */ 201: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Validation error */ 400: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Coin rule with this trigger already exists */ 409: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CoinRulesController_update: { parameters: { query?: never; header?: never; path: { /** @description UUID of the coin rule */ id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UpdateCoinRuleDto"]; }; }; responses: { /** @description Coin rule updated successfully */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Validation error */ 400: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Coin rule not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CoinRulesController_delete: { parameters: { query?: never; header?: never; path: { /** @description UUID of the coin rule */ id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Coin rule deleted successfully */ 204: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Coin rule not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CoinRulesController_rewardPoolTelemetry: { parameters: { query?: never; header?: never; path: { /** @description UUID of the coin rule */ id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Telemetry returned successfully */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Coin rule not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; EconomyController_getLedger: { parameters: { query?: { /** @description Filter by transaction direction */ direction?: "credit" | "debit"; /** @description Filter by transaction reason */ reason?: string; /** @description Page number (default: 1) */ page?: number; /** @description Items per page (default: 20) */ limit?: number; }; header?: never; path: { /** @description UUID of the buddy */ buddyId: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Paginated coin ledger returned successfully */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Buddy not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BadgeDefinitionsController_findAll: { parameters: { query: { audience: string; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description List of badge definitions returned successfully */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BadgeDefinitionsController_create: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["CreateBadgeDefinitionDto"]; }; }; responses: { /** @description Badge definition created successfully */ 201: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Validation error */ 400: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Badge definition with this key already exists */ 409: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BadgeDefinitionsController_uploadIcon: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Badge icon uploaded successfully */ 201: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Invalid file type or missing file */ 400: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BadgeDefinitionsController_generateIcon: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["GenerateBadgeIconDto"]; }; }; responses: { /** @description Badge icon generated successfully */ 201: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Validation error */ 400: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Image generation budget exceeded */ 402: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BadgeDefinitionsController_findById: { parameters: { query?: never; header?: never; path: { /** @description UUID of the badge definition */ id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Badge definition returned successfully */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Badge definition not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BadgeDefinitionsController_update: { parameters: { query?: never; header?: never; path: { /** @description UUID of the badge definition */ id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UpdateBadgeDefinitionDto"]; }; }; responses: { /** @description Badge definition updated successfully */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Validation error */ 400: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Badge definition not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BadgeDefinitionsController_delete: { parameters: { query?: never; header?: never; path: { /** @description UUID of the badge definition */ id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Badge definition deleted successfully */ 204: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Badge definition not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Cannot delete badge with existing awards */ 409: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BadgeDefinitionsController_regenerateIcon: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Regeneration queued */ 202: { headers: { [name: string]: unknown; }; content?: never; }; }; }; EventTriggeredBadgeController_list: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; EventTriggeredBadgeController_create: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["CreateEventBadgeDto"]; }; }; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; EventTriggeredBadgeController_remove: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 204: { headers: { [name: string]: unknown; }; content?: never; }; }; }; EventTriggeredBadgeController_update: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UpdateEventBadgeDto"]; }; }; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; StreakDefinitionsController_findAll: { parameters: { query: { audience: string; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description List of streak definitions */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; StreakDefinitionsController_create: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["CreateStreakDefinitionDto"]; }; }; responses: { /** @description Streak definition created */ 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; StreakDefinitionsController_findById: { parameters: { query?: never; header?: never; path: { /** @description UUID of the streak definition */ id: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; StreakDefinitionsController_update: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UpdateStreakDefinitionDto"]; }; }; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; StreakDefinitionsController_delete: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 204: { headers: { [name: string]: unknown; }; content?: never; }; }; }; ImageCostController_getUsage: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Image usage stats returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; ImageCostController_getUsageReport: { parameters: { query?: { /** @description Month in YYYY-MM format (defaults to current month) */ month?: string; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Image usage report returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; EventsController_findAll: { parameters: { query?: { /** @description Filter events by user identifier */ user_id?: string; /** @description Filter events by event type */ type?: string; /** @description Maximum number of events to return (1-100) */ limit?: number; /** @description Filter events from this ISO 8601 date */ date_from?: string; /** @description Filter events up to this ISO 8601 date */ date_to?: string; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Filtered list of events returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; EventsController_ingest: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["IngestEventDto"]; }; }; responses: { /** @description Event accepted and processed */ 201: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Invalid event payload */ 400: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Monthly event quota exceeded */ 402: { headers: { [name: string]: unknown; }; content?: never; }; }; }; EventsController_adminTrigger: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["IngestEventDto"]; }; }; responses: { /** @description Event accepted and processed */ 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; EventsController_ingestBatch: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["IngestBatchDto"]; }; }; responses: { /** @description Events accepted and processed */ 201: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Invalid events payload */ 400: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Monthly event quota exceeded */ 402: { headers: { [name: string]: unknown; }; content?: never; }; }; }; EventsController_findTypes: { parameters: { query: { audience: string; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Distinct event type list returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; EventsController_findActiveUsers: { parameters: { query?: { /** @description Window in hours to look back (1-168) */ hours?: number; /** @description Maximum number of users to return (1-200) */ limit?: number; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Active users returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; EventsController_findById: { parameters: { query?: never; header?: never; path: { /** @description Event UUID */ id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Event returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Event not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; EventTypesController_findAll: { parameters: { query: { audience: string; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; EventTypesController_create: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["CreateEventTypeDto"]; }; }; responses: { /** @description Event type created */ 201: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Event type already exists */ 409: { headers: { [name: string]: unknown; }; content?: never; }; }; }; EventTypesController_findById: { parameters: { query?: never; header?: never; path: { /** @description Event type UUID */ id: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; EventTypesController_update: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UpdateEventTypeDto"]; }; }; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; EventTypesController_delete: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 204: { headers: { [name: string]: unknown; }; content?: never; }; }; }; TokenConfigController_list: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Token configurations returned successfully */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; }; }; TokenConfigController_upsert: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UpsertTokenConfigDto"]; }; }; responses: { /** @description Token configuration upserted successfully */ 201: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Validation error */ 400: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unauthorized */ 401: { headers: { [name: string]: unknown; }; content?: never; }; }; }; MarketplaceController_list: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description List of marketplaces returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; MarketplaceController_create: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["CreateMarketplaceDto"]; }; }; responses: { /** @description Marketplace created successfully */ 201: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Invalid input */ 400: { headers: { [name: string]: unknown; }; content?: never; }; }; }; MarketplaceController_findOne: { parameters: { query?: never; header?: never; path: { /** @description Marketplace UUID */ id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Marketplace returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Marketplace not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; MarketplaceController_update: { parameters: { query?: never; header?: never; path: { /** @description Marketplace UUID */ id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UpdateMarketplaceDto"]; }; }; responses: { /** @description Marketplace updated successfully */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Marketplace not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; MarketplaceController_listItems: { parameters: { query?: { /** @description Filter by category */ category?: string; /** @description Filter by rarity tier */ rarity?: string; /** @description Only show active items */ active_only?: string; /** @description Page number */ page?: string; /** @description Items per page */ limit?: string; }; header?: never; path: { /** @description Marketplace UUID */ id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Paginated list of items returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; MarketplaceController_createItem: { parameters: { query?: never; header?: never; path: { /** @description Marketplace UUID */ id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["CreateItemDto"]; }; }; responses: { /** @description Item created successfully */ 201: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Invalid input */ 400: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Marketplace not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; MarketplaceController_importItems: { parameters: { query?: never; header?: never; path: { /** @description Marketplace UUID */ id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["ImportMarketplaceItemsDto"]; }; }; responses: { /** @description Items imported successfully */ 201: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Invalid import payload */ 400: { headers: { [name: string]: unknown; }; content?: never; }; }; }; MarketplaceController_findItem: { parameters: { query?: never; header?: never; path: { /** @description Marketplace UUID */ id: string; /** @description Item UUID */ item_id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Item returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Item not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; MarketplaceController_updateItem: { parameters: { query?: never; header?: never; path: { /** @description Marketplace UUID */ id: string; /** @description Item UUID */ item_id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UpdateItemDto"]; }; }; responses: { /** @description Item updated successfully */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Item not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; MarketplaceController_deleteItem: { parameters: { query?: never; header?: never; path: { /** @description Marketplace UUID */ id: string; /** @description Item UUID */ item_id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Item deleted successfully */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Item not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; MarketplaceController_reorderItems: { parameters: { query?: never; header?: never; path: { /** @description Marketplace UUID */ id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["ReorderItemsDto"]; }; }; responses: { /** @description Items reordered successfully */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; MarketplaceController_uploadItemImage: { parameters: { query?: never; header?: never; path: { /** @description Marketplace UUID */ id: string; /** @description Item UUID */ item_id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Item image uploaded successfully */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Invalid file or missing image */ 400: { headers: { [name: string]: unknown; }; content?: never; }; }; }; MarketplaceController_regenerateItemImage: { parameters: { query?: never; header?: never; path: { /** @description Marketplace UUID */ id: string; /** @description Item UUID */ item_id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Regeneration queued */ 202: { headers: { [name: string]: unknown; }; content?: never; }; }; }; MarketplaceWidgetController_gift: { parameters: { query?: never; header?: never; path: { /** @description Item UUID */ id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["GiftItemDto"]; }; }; responses: { /** @description Item gifted (or idempotent replay) */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Sender has insufficient coins */ 402: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Missing marketplace:purchase scope */ 403: { headers: { [name: string]: unknown; }; content?: never; }; }; }; FlashSaleController_list: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; FlashSaleController_schedule: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["ScheduleFlashSaleDto"]; }; }; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; FlashSaleController_cancel: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; MarketplaceFomoWidgetController_resolve: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["MarketplaceFomoQueryDto"]; }; }; responses: { /** @description FOMO signals returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; LegacyEquipController_getReturningChampion: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Returning-champion view returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; LegacyEquipController_dismissReturningChampion: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Scene dismissed */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; LegacyEquipController_equipLegacyItem: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["EquipLegacyItemDto"]; }; }; responses: { /** @description Legacy crown temp-equipped */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Item not owned or not a crown */ 400: { headers: { [name: string]: unknown; }; content?: never; }; }; }; ReturningChampionPublicController_resolveWelcomeBack: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Widget session minted */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Invalid token or tenant slug */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WebhooksController_list: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description List of webhook configs returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WebhooksController_create: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["CreateWebhookConfigDto"]; }; }; responses: { /** @description Webhook config created with signing secret */ 201: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Validation failed */ 422: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WebhooksController_listEvents: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Webhook event types returned */ 200: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["WebhookEventTypesResponseDto"]; }; }; }; }; WebhooksController_getHealth: { parameters: { query?: { /** @description Lookback window in days (default: 7, max: 30) */ days?: string; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Webhook health returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WebhooksController_findOne: { parameters: { query?: never; header?: never; path: { /** @description Webhook config UUID */ id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Webhook config returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Webhook config not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WebhooksController_update: { parameters: { query?: never; header?: never; path: { /** @description Webhook config UUID */ id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UpdateWebhookConfigDto"]; }; }; responses: { /** @description Webhook config updated */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Webhook config not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WebhooksController_delete: { parameters: { query?: never; header?: never; path: { /** @description Webhook config UUID */ id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Webhook config deleted */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Webhook config not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WebhooksController_rotateSecret: { parameters: { query?: never; header?: never; path: { /** @description Webhook config UUID */ id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Webhook secret rotated */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Webhook config not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WebhooksController_sendTest: { parameters: { query?: never; header?: never; path: { /** @description Webhook config UUID */ id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Test webhook delivered */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Webhook config not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WebhooksController_getDeliveries: { parameters: { query?: { /** @description Maximum number of deliveries to return */ limit?: string; }; header?: never; path: { /** @description Webhook config UUID */ id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Delivery log returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Webhook config not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; WebhooksController_redeliver: { parameters: { query?: never; header?: never; path: { /** @description Webhook config UUID */ id: string; /** @description Delivery log UUID */ deliveryId: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Redelivery enqueued */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Delivery log not found */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; PrestigeController_status: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; PrestigeController_prestige: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; AnalyticsController_getOverview: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Analytics overview returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; AnalyticsController_getEngagement: { parameters: { query?: { /** @description Aggregation period */ period?: "daily" | "weekly" | "monthly"; /** @description Number of days to look back (default: 30) */ days?: number; /** @description Start date (ISO 8601) */ from?: string; /** @description End date (ISO 8601) */ to?: string; /** @description Optional audience key filter */ audience?: string; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Engagement metrics returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; AnalyticsController_getActivitySummary: { parameters: { query?: { /** @description Number of days to look back (default: 30) */ days?: number; /** @description Start date (ISO 8601) */ from?: string; /** @description End date (ISO 8601) */ to?: string; /** @description Optional audience key filter */ audience?: string; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Activity summary returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; AnalyticsController_getEconomySummary: { parameters: { query?: { /** @description Number of days to look back (default: 30) */ days?: number; /** @description Start date (ISO 8601) */ from?: string; /** @description End date (ISO 8601) */ to?: string; /** @description Optional audience key filter */ audience?: string; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Economy summary returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; AnalyticsController_getEconomyHealth: { parameters: { query?: { /** @description Number of days to look back (default: 30) */ days?: number; /** @description Start date (ISO 8601) */ from?: string; /** @description End date (ISO 8601) */ to?: string; /** @description Optional audience key filter */ audience?: string; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Economy health metrics returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; AnalyticsController_getPopularItems: { parameters: { query?: { /** @description Number of items to return (default: 10) */ limit?: number; /** @description Number of days to look back (default: 30) */ days?: number; /** @description Start date (ISO 8601) */ from?: string; /** @description End date (ISO 8601) */ to?: string; /** @description Optional audience key filter */ audience?: string; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Popular items list returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; AnalyticsController_getPopularBadges: { parameters: { query?: { /** @description Number of badges to return (default: 10) */ limit?: number; /** @description Number of days to look back (default: 30) */ days?: number; /** @description Start date (ISO 8601) */ from?: string; /** @description End date (ISO 8601) */ to?: string; /** @description Optional audience key filter */ audience?: string; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Popular badges list returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; AnalyticsController_getRetention: { parameters: { query?: { /** @description Cohort period */ cohort?: "weekly" | "monthly"; /** @description Number of days to look back (default: 90) */ days?: number; /** @description Start date (ISO 8601) */ from?: string; /** @description End date (ISO 8601) */ to?: string; /** @description Optional audience key filter */ audience?: string; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Retention metrics returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; AnalyticsController_getRoiMetrics: { parameters: { query?: { /** @description Number of days to look back (default: 30) */ days?: number; /** @description Start date (ISO 8601) */ from?: string; /** @description End date (ISO 8601) */ to?: string; /** @description Optional audience key filter */ audience?: string; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description ROI metrics returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; AnalyticsController_getStreakHealth: { parameters: { query?: { /** @description Optional audience key filter */ audience?: string; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Streak health returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; AnalyticsController_getTokenEconomy: { parameters: { query?: { /** @description Optional audience key filter */ audience?: string; /** @description Lookback window in days (default: 30) */ days?: number; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Token economy returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; AnalyticsController_getAudienceBreakdown: { parameters: { query?: { /** @description Lookback window in days (default: 30) */ days?: number; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Audience breakdown returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; AnalyticsController_getEvolutionTimeline: { parameters: { query?: { /** @description Optional audience key filter */ audience?: string; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Evolution timeline returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; AnalyticsController_getMarketplaceFunnel: { parameters: { query?: { /** @description Optional audience key filter */ audience?: string; /** @description Max rows to return (default: 10) */ limit?: number; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Marketplace funnel returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; AnalyticsController_getWebhookHealth: { parameters: { query?: { /** @description Lookback window in days (default: 7) */ days?: number; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Webhook health returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; AnalyticsController_getEventCounts: { parameters: { query?: { audience?: string; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Per-event counts returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; AnalyticsController_getCustomEventTrends: { parameters: { query?: { /** @description Optional audience key filter */ audience?: string; /** @description Lookback window in days (default: 30) */ days?: number; /** @description Max event types to return (default: 10) */ limit?: number; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Custom event trends returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; AnalyticsController_getFeatureActivity: { parameters: { query?: { /** @description Optional audience key filter */ audience?: string; /** @description Lookback window in weeks (default: 8) */ weeks?: number; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Feature activity returned */ 200: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["FeatureActivityResponseDto"]; }; }; }; }; LeaderboardController_getLeaderboard: { parameters: { query?: { /** @description Metric to rank by */ metric?: "total_xp" | "coins" | "badge_count" | "evolution_stage"; /** @description Time period for ranking */ period?: "daily" | "weekly" | "monthly" | "all_time"; /** @description Number of entries to return (default: 50) */ limit?: number; /** @description Override tenant default view mode for this request */ view_mode?: "top" | "around_me" | "hybrid"; /** @description Legacy shorthand override for view_mode=around_me */ around_me?: boolean; /** @description Hybrid mode: how many leaders to show in the top strip (default 3) */ top_strip_size?: number; /** @description HTCH-48: override tenant default scope. friends → 501 until Phase 3. */ scope?: "global" | "team" | "friends"; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Leaderboard entries returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; LeaderboardConfigController_get: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; LeaderboardConfigController_patch: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; ShareFunnelController_giftFunnel: { parameters: { query?: { /** @description Rolling window (default 30) */ window_days?: number; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BillingController_getStatus: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Billing status returned */ 200: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["BillingStatusResponseDto"]; }; }; }; }; BillingController_createCheckout: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Checkout session URL returned */ 201: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Invalid plan or bundle key */ 400: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BillingController_reconcileCheckout: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Checkout session reconciled; billing state returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Session does not belong to the caller */ 400: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BillingController_createPortal: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Portal session URL returned */ 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CreditsController_getBalance: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Credit balance returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CreditsController_listLedger: { parameters: { query: { cursor: string; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Ledger entries returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; OnboardingController_create: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; OnboardingController_preparingStatus: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; OnboardingController_seedFromUrl: { parameters: { query?: never; header: { origin: string; }; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["SeedFromUrlDto"]; }; }; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; OnboardingController_seedFromRepo: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["SeedFromRepoDto"]; }; }; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; OnboardingController_seedFromDescription: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["SeedFromDescriptionDto"]; }; }; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; OnboardingController_joinWaitlist: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["WaitlistDto"]; }; }; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; OnboardingController_current: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; OnboardingController_reset: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; OnboardingController_updateAnswers: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UpdateAnswersDto"]; }; }; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; OnboardingController_message: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["SendMessageDto"]; }; }; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; OnboardingController_generatePlan: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; OnboardingController_regeneratePlan: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; OnboardingController_apply: { parameters: { query?: never; header: { "if-match": string; }; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; OnboardingController_generateGuide: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; StageAssetsController_list: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; StageAssetsController_createUploadUrl: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["CreateUploadUrlDto"]; }; }; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; StageAssetsController_regenerate: { parameters: { query?: never; header?: never; path: { stage: number; }; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; StageAssetsController_commit: { parameters: { query?: never; header?: never; path: { stage: number; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["CommitAssetDto"]; }; }; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; StageAssetsController_remove: { parameters: { query?: never; header?: never; path: { stage: number; }; cookie?: never; }; requestBody?: never; responses: { 204: { headers: { [name: string]: unknown; }; content?: never; }; }; }; GatesController_list: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; GatesController_upsert: { parameters: { query?: never; header?: never; path: { gate_key: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UpsertTokenGateDto"]; }; }; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; GatesController_remove: { parameters: { query?: never; header?: never; path: { gate_key: string; }; cookie?: never; }; requestBody?: never; responses: { 204: { headers: { [name: string]: unknown; }; content?: never; }; }; }; GatesController_unlock: { parameters: { query?: never; header?: never; path: { buddy_id: string; gate_key: string; }; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; /** @description insufficient_balance or progression_not_spendable */ 400: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Gate not found / inactive */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; GatesController_listUnlocks: { parameters: { query?: never; header?: never; path: { buddy_id: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; PathsController_listDefinitions: { parameters: { query: { audience: string; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; PathsController_createDefinition: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["CreatePathDefinitionDto"]; }; }; responses: { /** @description Path definition created */ 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; PathsController_getDefinition: { parameters: { query?: never; header?: never; path: { /** @description UUID of the path definition */ id: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; PathsController_updateDefinition: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UpdatePathDefinitionDto"]; }; }; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; PathsController_deleteDefinition: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 204: { headers: { [name: string]: unknown; }; content?: never; }; }; }; PathsController_activate: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; PathsController_deactivate: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; PathsController_listSteps: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; PathsController_createStep: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["CreatePathStepDto"]; }; }; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; PathsController_reorderSteps: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["ReorderDto"]; }; }; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; PathsController_updateStep: { parameters: { query?: never; header?: never; path: { id: string; stepId: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UpdatePathStepDto"]; }; }; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; PathsController_deleteStep: { parameters: { query?: never; header?: never; path: { id: string; stepId: string; }; cookie?: never; }; requestBody?: never; responses: { 204: { headers: { [name: string]: unknown; }; content?: never; }; }; }; PathsController_listSubSteps: { parameters: { query?: never; header?: never; path: { id: string; stepId: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; PathsController_createSubStep: { parameters: { query?: never; header?: never; path: { id: string; stepId: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["CreatePathSubStepDto"]; }; }; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; PathsController_reorderSubSteps: { parameters: { query?: never; header?: never; path: { id: string; stepId: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["ReorderDto"]; }; }; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; PathsController_updateSubStep: { parameters: { query?: never; header?: never; path: { id: string; stepId: string; subStepId: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UpdatePathSubStepDto"]; }; }; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; PathsController_deleteSubStep: { parameters: { query?: never; header?: never; path: { id: string; stepId: string; subStepId: string; }; cookie?: never; }; requestBody?: never; responses: { 204: { headers: { [name: string]: unknown; }; content?: never; }; }; }; PathsController_getBuddyPath: { parameters: { query?: never; header?: never; path: { buddyId: string; pathKey: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; PathsController_manualComplete: { parameters: { query?: never; header?: never; path: { buddyId: string; pathKey: string; subKey: string; }; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BeginnersLuckController_getAnalytics: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BeginnersLuckWidgetController_getResult: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Beginner's Luck result returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; FreeLunchWidgetController_getNotification: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Free Lunch notification returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; FreeLunchWidgetController_acknowledge: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Acknowledgement recorded */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; TeamsAdminController_list: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; TeamsAdminController_create: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; TeamsAdminController_remove: { parameters: { query?: never; header?: never; path: { teamId: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; TeamsAdminController_update: { parameters: { query?: never; header?: never; path: { teamId: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; TeamsAdminController_listMembers: { parameters: { query?: never; header?: never; path: { teamId: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; TeamsAdminController_addMember: { parameters: { query?: never; header?: never; path: { teamId: string; }; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; TeamsAdminController_removeMember: { parameters: { query?: never; header?: never; path: { teamId: string; buddyId: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; TeamsAdminController_changeRole: { parameters: { query?: never; header?: never; path: { teamId: string; buddyId: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; TeamsAdminController_import: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; TeamsWidgetController_me: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; TeamsWidgetController_leave: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; KudoTypesAdminController_list: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; KudoTypesAdminController_create: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; KudoTypesAdminController_applyTemplate: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; KudoTypesAdminController_applyThemeTemplate: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; KudoTypesAdminController_reorder: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; KudoTypesAdminController_remove: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; KudoTypesAdminController_update: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; KudosWidgetController_types: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; KudosWidgetController_send: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; KudosWidgetController_received: { parameters: { query?: { limit?: string; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; KudosWidgetController_given: { parameters: { query?: { limit?: string; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; GroupQuestsAdminController_list: { parameters: { query: { status: string; team_id: string; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; GroupQuestsAdminController_create: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; GroupQuestsAdminController_remove: { parameters: { query?: never; header?: never; path: { questId: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; GroupQuestsAdminController_update: { parameters: { query?: never; header?: never; path: { questId: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; GroupQuestsAdminController_publish: { parameters: { query?: never; header?: never; path: { questId: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; GroupQuestsAdminController_forceResolve: { parameters: { query?: never; header?: never; path: { questId: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; GroupQuestsWidgetController_active: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; GroupQuestsWidgetController_join: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; GroupQuestsWidgetController_leave: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; MentorWidgetController_setAvailability: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; MentorWidgetController_teamMentors: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; MentorWidgetController_logSession: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; MentorWidgetController_mySessions: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; MentorAdminController_getConfig: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; MentorAdminController_updateConfig: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; MentorAdminController_directory: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; MentorAdminController_resetSessions: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BragWidgetController_shareProfile: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BragWidgetController_winState: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Win-State brag payload + enabled channels */ 200: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["WinStateBragResponseDto"]; }; }; }; }; BragWidgetController_slackPost: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BragWidgetController_telemetry: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BragAdminController_getConfig: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BragAdminController_updateConfig: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BragAdminController_webhookTest: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BragAdminController_funnel: { parameters: { query?: { from?: string; to?: string; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BragAdminController_byChannel: { parameters: { query?: { from?: string; to?: string; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BragAdminController_telemetryCsv: { parameters: { query?: { from?: string; to?: string; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; SocialNormsWidgetController_today: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; FeedWidgetController_listTeamEvents: { parameters: { query?: { cursor?: string; limit?: number; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; FeedWidgetController_clap: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CauseAdminController_list: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CauseAdminController_create: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CauseAdminController_audit: { parameters: { query?: { page?: string; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CauseAdminController_previewConfig: { parameters: { query: { trigger_events: string; rate: string; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CauseAdminController_analyticsView: { parameters: { query?: { period?: "7d" | "30d" | "90d" | "lifetime"; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CauseAdminController_analyticsCsv: { parameters: { query?: { period?: "7d" | "30d" | "90d" | "lifetime"; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CauseAdminController_remove: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CauseAdminController_update: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CauseAdminController_draft: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CauseAdminController_preview: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CauseAdminController_getWebhook: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CauseAdminController_updateWebhook: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CauseAdminController_testWebhook: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CauseWidgetController_counters: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CauseWidgetController_surfaces: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; FoundationsController_widgetList: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CouncilAdminController_list: { parameters: { query?: { status?: "pending" | "approved" | "rejected" | "live"; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CouncilAdminController_moderate: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CouncilAdminController_promote: { parameters: { query?: never; header?: never; path: { slot: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CouncilAdminController_revert: { parameters: { query?: never; header?: never; path: { slot: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CouncilWidgetController_mine: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; CouncilWidgetController_create: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; FoundingCohortAdminController_preview: { parameters: { query?: { mode?: "first_n" | "first_percent" | "first_until_date"; first_n?: string; first_percent?: string; until_date?: string; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; FoundingCohortAdminController_backfill: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; FoundingCohortAdminController_audit: { parameters: { query?: { page?: string; source?: "real_time" | "backfill"; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; FoundingCohortAdminController_exportAuditCsv: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; FoundingCohortWidgetController_status: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; NotificationWidgetController_list: { parameters: { query?: { cursor?: string; limit?: string; include_read?: string; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; NotificationWidgetController_unreadCount: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; NotificationWidgetController_dismissAll: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; NotificationWidgetController_read: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; NotificationWidgetController_dismiss: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; NotificationWidgetController_snooze: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; SunkCostController_summary: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; SunkCostController_acknowledge: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; VacationWidgetController_pause: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; VacationWidgetController_resume: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; VacationWidgetController_status: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; VacationAdminController_analytics: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; StreakWatchdogAdminController_analytics: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; ProfileHistoryController_getHistory: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; ProfileHistoryController_reclaim: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; MysteryBoxController_state: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; MysteryBoxController_claim: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; SurpriseDropsController_list: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; SurpriseDropsController_create: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["CreateSurpriseDropDto"]; }; }; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; SurpriseDropsController_remove: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 204: { headers: { [name: string]: unknown; }; content?: never; }; }; }; SurpriseDropsController_update: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UpdateSurpriseDropDto"]; }; }; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BoostersAdminController_grant: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["AdminGrantBoosterDto"]; }; }; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BoostersController_active: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BoostersController_catalog: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; BoostersController_purchase: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["PurchaseBoosterDto"]; }; }; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; LotteriesController_list: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; LotteriesController_create: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["CreateLotteryDto"]; }; }; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; LotteriesController_remove: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 204: { headers: { [name: string]: unknown; }; content?: never; }; }; }; LotteriesController_update: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UpdateLotteryDto"]; }; }; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; LotteriesController_draws: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; LotteriesController_previewNextDraw: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; LotteriesController_simulateDraw: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; LotteryWidgetController_activeEntries: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; LotteryWidgetController_lastWin: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; ProfileTemplateController_list: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; ProfileTemplateController_create: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["CreateProfileTemplateDto"]; }; }; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; ProfileTemplateController_remove: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 204: { headers: { [name: string]: unknown; }; content?: never; }; }; }; ProfileTemplateController_update: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UpdateProfileTemplateDto"]; }; }; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; ProfileTemplateController_applyBulk: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["BulkApplyDto"]; }; }; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; LeagueWidgetController_me: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; LeagueWidgetController_bossFight: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; LeagueHighlightsController_latest: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Season-closing discovery payload */ 200: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["SeasonClosingDiscoveryResponseDto"]; }; }; }; }; LeagueHighlightsController_me: { parameters: { query?: never; header?: never; path: { seasonId: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; OffSeasonController_status: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; OffSeasonController_startScoutingQuest: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["StartScoutingQuestDto"]; }; }; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; OffSeasonController_joinScoutingQuest: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; LeagueAdminController_getConfig: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; LeagueAdminController_updateConfig: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UpdateLeagueConfigDto"]; }; }; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; LeagueAdminController_replaceTiers: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["ReplaceLeagueTiersDto"]; }; }; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; LeagueAdminController_previewSeasons: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; LeagueAdminController_scheduleSeason: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["ScheduleLeagueSeasonDto"]; }; }; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; LeagueAdminController_forceClose: { parameters: { query?: never; header?: never; path: { seasonId: string; }; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; HallOfFameController_getList: { parameters: { query?: never; header?: never; path: { tenantSlug: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Hall of Fame season list */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description No finalized seasons / sharing off */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; HallOfFameController_getSeason: { parameters: { query?: never; header?: never; path: { tenantSlug: string; seasonId: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Hall of Fame season detail */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unknown season / sharing off */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; HallOfFameIndexController_getIndex: { parameters: { query: { offset: number; limit: number; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Paged Hall of Fame URL index */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; HexadSurveyController_questions: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; HexadSurveyController_submit: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; HexadSurveyController_me: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; HexadSurveyController_deleteMe: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 204: { headers: { [name: string]: unknown; }; content?: never; }; }; }; MarketingAnalyticsController_recordCtaClick: { parameters: { query?: never; header: { "user-agent": string; }; path?: never; cookie?: never; }; requestBody?: never; responses: { 202: { headers: { [name: string]: unknown; }; content?: never; }; }; }; ShowroomAdminController_list: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; ShowroomAdminController_create: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["CreateShowroomDto"]; }; }; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; ShowroomAdminController_getOne: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; ShowroomAdminController_update: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UpdateShowroomDto"]; }; }; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; ShowroomAdminController_publish: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; ShowroomAdminController_unpublish: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; ShowroomAdminController_regenerateQr: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; ShowroomAdminController_archive: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; ShowroomAdminController_award: { parameters: { query?: never; header: { "idempotency-key": string; }; path: { buddyId: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["AwardDto"]; }; }; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; ShowroomAdminController_listPlayerAwards: { parameters: { query: { limit: number; }; header?: never; path: { buddyId: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; ShowroomAdminController_listAudit: { parameters: { query: { page: number; page_size: number; }; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; PublicShowroomController_getPublicShowroom: { parameters: { query: { qr_token: string; qr: string; }; header?: never; path: { slug: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Public Showroom view */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unknown slug or page is not published */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; PublicShowroomController_getQrPayload: { parameters: { query?: never; header?: never; path: { slug: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description QR payload */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Unknown slug or page is not published */ 404: { headers: { [name: string]: unknown; }; content?: never; }; }; }; HostedSurfacesAdminController_list: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; HostedSurfacesAdminController_create: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["CreateSurfaceDto"]; }; }; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; HostedSurfacesAdminController_getOne: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; HostedSurfacesAdminController_update: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UpdateSurfaceDto"]; }; }; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; HostedSurfacesAdminController_readiness: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { /** @description Per-widget readiness entries */ 200: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["SurfaceWidgetReadinessResponseDto"]; }; }; }; }; HostedSurfacesAdminController_uploadLogo: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; HostedSurfacesAdminController_publish: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; HostedSurfacesAdminController_unpublish: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; HostedSurfacesAdminController_archive: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; HostedSurfacesAdminController_listPlayers: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; HostedSurfacesAdminController_createPlayer: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["CreatePlayerDto"]; }; }; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; HostedSurfacesAdminController_updatePlayer: { parameters: { query?: never; header?: never; path: { id: string; playerId: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UpdatePlayerDto"]; }; }; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; HostedSurfacesAdminController_regenerateAccess: { parameters: { query?: never; header?: never; path: { id: string; playerId: string; }; cookie?: never; }; requestBody?: never; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; HostedSurfacesAdminController_revealAccess: { parameters: { query?: never; header?: never; path: { id: string; playerId: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; HostedSurfacesAdminController_listRecipes: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; HostedSurfacesAdminController_upsertRecipe: { parameters: { query?: never; header?: never; path: { id: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UpsertRecipeDto"]; }; }; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; HostedSurfacesAdminController_runRecipe: { parameters: { query?: never; header?: never; path: { id: string; key: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["RunRecipeDto"]; }; }; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; PublicHostedSurfacesController_getPublic: { parameters: { query?: never; header?: never; path: { slug: string; }; cookie?: never; }; requestBody?: never; responses: { 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; PublicHostedSurfacesController_startSession: { parameters: { query?: never; header: { "user-agent": string; }; path: { slug: string; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["StartSessionDto"]; }; }; responses: { 201: { headers: { [name: string]: unknown; }; content?: never; }; }; }; HealthController_check: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Health status returned with component details */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; HealthController_readiness: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description All dependencies up */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description One or more critical dependencies unavailable */ 503: { headers: { [name: string]: unknown; }; content?: never; }; }; }; HealthController_liveness: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Process is alive */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; HealthController_version: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Build metadata returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; ProbeController_healthz: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Process is alive */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; ProbeController_readyz: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Service is ready to serve traffic */ 200: { headers: { [name: string]: unknown; }; content?: never; }; /** @description Database or Redis is not reachable */ 503: { headers: { [name: string]: unknown; }; content?: never; }; }; }; ProbeController_version: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; requestBody?: never; responses: { /** @description Build metadata returned */ 200: { headers: { [name: string]: unknown; }; content?: never; }; }; }; } ``` ### `OffsetPage` Pagination helpers — `paginate()` and `collect()`. Hatched list endpoints ship in two shapes: - **Offset (legacy)** — `{ data, meta: { total, page, limit } }`. The helper drives `fetchPage(page)` with a 1-indexed page number and stops on the last page. - **Cursor (canonical)** — `{ data, pagination: { nextCursor, hasMore, limit } }`. The helper drives `fetchPage(cursor)` with `undefined` first and the server-returned `nextCursor` thereafter; iteration stops when `nextCursor === null`. `paginate()` autodetects the shape from the first response — you don't have to know which one your endpoint serves. @example Cursor pagination (canonical) ```ts for await (const op of paginate((cursor) => hatched.operations.list({ cursor, limit: 100 }))) { console.log(op.id); } ``` @example Offset pagination (legacy) ```ts for await (const buddy of paginate((page) => hatched.buddies.list({ page, limit: 100 }))) { console.log(buddy.id); } ``` ```ts export interface OffsetPage { data: T[]; meta: { total: number; page: number; limit: number }; } ``` ### `CursorPage` ```ts export interface CursorPage { data: T[]; pagination: { nextCursor: string | null; hasMore: boolean; limit: number }; } ``` ### `PaginatedResponse` ```ts export type PaginatedResponse = OffsetPage | CursorPage; ``` ### `PaginateOptions` ```ts export interface PaginateOptions { /** * Maximum number of pages to walk. Useful as a runaway guard in scripts * where you want to bail out after, say, 50 pages even if the API * reports more available. Defaults to `Infinity`. */ maxPages?: number; /** * AbortSignal to abort the iteration. The current in-flight request is * aborted via the fetcher (if it forwards `signal`), and iteration * stops immediately. */ signal?: AbortSignal; } ``` ### `WebhookPayload` ```ts export type WebhookPayload = Record; ``` ### `VerifyResult` ```ts export interface VerifyResult { /** True only when both the signature is valid and the body parses as JSON. */ valid: boolean; /** Parsed raw event payload when `valid` is true; null when verification or parsing failed. */ event: WebhookPayload | null; /** `X-Hatched-Event` header value. The event name is not wrapped into the body. */ eventType: WebhookEvent | string | null; /** `X-Hatched-Delivery` header value. Use this as the dedupe key. */ deliveryId: string | null; /** Reason a verification failed. Useful for logs/observability. */ reason?: | 'missing_header' | 'missing_secret' | 'missing_body' | 'invalid_signature' | 'invalid_json'; } ``` ### `VerifyAdapterOptions` ```ts export interface VerifyAdapterOptions { /** Allowed clock skew between Hatched and the consumer host. Default 300s. */ toleranceSeconds?: number; /** Custom now() — useful for testing replay-window edge cases. */ now?: () => number; } ``` ## Related - [Error codes](/docs/reference/error-codes) - [HTTP API](/docs/reference/http-api) - [Getting started](/docs/guides/getting-started) --- # SDK (React Native) > Component and hook reference for @hatched/react-native — HatchedProvider, the native widgets, HatchedWebView, and hooks. Source: https://docs.hatched.live/docs/reference/react-native Package: [`@hatched/react-native`](https://npmjs.com/package/@hatched/react-native) ```bash npx expo install @hatched/react-native # optional peers npx expo install react-native-webview react-native-svg ``` For the end-to-end walkthrough (tokens, theming, WebView fallback, pitfalls) see [React Native integration](/docs/guides/react-native-integration). This page is the API surface. ## `` Owns the shared data layer and must wrap every widget and hook. | Prop | Type | Required | Notes | | --- | --- | --- | --- | | `token` | `string` | yes | Widget session token or read-only embed token (server-minted). | | `apiBaseUrl` | `string` | yes | e.g. `https://api.hatched.live/api/v1`. | | `theme` | `'light' \| 'dark'` | no | Initial mode; the live `/widget/theme` palette/personality overrides it. | | `lang` | `string` | no | UI language hint (defaults to English). | | `pollIntervalMs` | `number` | no | Base `/widget/state` poll interval. | | `onCelebrate` | `(event) => void` | no | Fired on badge / evolution / streak celebrations. | The provider polls `/widget/state`, pauses while the app is backgrounded (`AppState`), honors the OS reduced-motion setting, and re-resolves the theme on foreground. ## Widgets All widgets accept an optional `style` prop and must render inside ``. | Component | Props | Surface | | --- | --- | --- | | `BuddyWidget` | `allowRadar?: boolean` | Buddy image, name, coins, skills (bars or SVG radar), badge/rank counters, evolution-ready hint. | | `LeaderboardWidget` | `scope?: 'global' \| 'team'`, `viewMode?: 'top' \| 'around_me' \| 'hybrid'`, `limit?: number` | Ranked board with the current player highlighted. | | `MarketplaceWidget` | — | Browse, buy, equip. CTAs are scope-gated. | | `StreakWidget` | `streakKey?: string` | Current streak, best, next milestone. | | `BadgesWidget` | — | Earned badges + earned/total counter. | | `TokensWidget` | — | Primary balance + capped progression tokens. | | `PathWidget` | `pathKey?: string` | Journey steps/sub-steps; tappable optimistic completion. | `StreakWidget`, `PathWidget`, and `LeaderboardWidget` fetch their own endpoint; the rest read state hydrated by the provider's `/widget/state` poll. ## `` Renders a Hatched widget through the production CDN loader for the surfaces not reimplemented natively. | Prop | Type | Notes | | --- | --- | --- | | `widget` | `string` | One of `kudos`, `group-quest`, `feed`, `mystery-box`, `league`, `council`, `hexad-survey`, `celebrate`. Required. | | `token` | `string` | Falls back to the provider. | | `apiBaseUrl` | `string` | Falls back to the provider. | | `theme` | `'light' \| 'dark'` | Falls back to the provider's mode, else `light`. | | `cdnBaseUrl` | `string` | Loader origin; defaults to `https://cdn.hatched.live`. | | `onEvent` | `(name, payload?) => void` | `ready` / `effects` from the bridge. | | `onCelebrate` | `(event) => void` | Celebration events from the widget. | | `syncStore` | `boolean` | Forward `track` effects into the provider's store (default `true`). | Requires the `react-native-webview` peer. The token is injected after load (never in the HTML); the loader's bridge forwards effects/celebrations back to the shared store. ## Hooks | Hook | Returns | | --- | --- | | `useTrack()` | `(type: string, properties?: Record) => Promise` | | `useHatched()` | `{ store, token, apiBaseUrl, ready, track, refresh }` | | `useTheme()` | the resolved `ResolvedTheme` | | `useWorkspaceState()` | the full `WorkspaceState` (re-renders on change) | | `useWorkspaceStore(selector)` | `selector(WorkspaceState)`, re-rendered on change | ## Optional peers | Peer | Needed for | Without it | | --- | --- | --- | | `react-native-webview` | `` | Mounting it throws a clear install error. | | `react-native-svg` | Buddy skill radar | Buddy widget falls back to skill bars. | | `@react-native-async-storage/async-storage` | Persisting ceremony-seen state | Falls back to in-memory. | ## `@hatched/widget-core` `@hatched/react-native` builds on [`@hatched/widget-core`](https://npmjs.com/package/@hatched/widget-core) — the headless, render-free core (workspace store, smart-poll, fetch/auth, theme resolution, shared types) that also powers the web widgets. You normally consume it transitively; install it directly only if you build a custom renderer. The commonly needed types (`ResolvedTheme`, `WorkspaceState`, `BuddyData`, `TokensPayload`, …) are re-exported from `@hatched/react-native`. --- # Buddy widget > Animated companion with stage, coins, equipped items, and live event effects. Source: https://docs.hatched.live/docs/reference/widgets/buddy ## Mount ```html
``` Use `data-embed-token` on the script instead of `data-session-token` for a read-only buddy. For client-rendered routes, call `window.__HATCHED_WIDGET__?.init({ token })` after the mount element exists. The global is `window.__HATCHED_WIDGET__`; the init payload uses `token`, not `embedToken`. ## Script attributes | Attribute | Values | Default | | -------------------- | ---------------------- | --------------------------------- | | `data-session-token` | widget session token | interactive mode | | `data-embed-token` | embed token | read-only mode | | `data-theme` | `light` `dark` | `light` | | `data-api-base-url` | API origin + `/api/v1` | `https://api.hatched.live/api/v1` | ## Required scopes - `read` renders buddy state. - `events:track` enables `window.__HATCHED_WIDGET__.track(...)`. ## Appearance and pending operations The buddy widget displays `buddy.appearance` from `/widget/state`. When an equip or evolve operation is still rendering marketplace items, the widget keeps the latest safe `image_url` visible and shows a small status banner: - `pending` while an appearance job is running. - `awaiting_credits` while Hatched waits for image credits before retrying. - `failed` when recovery or rerender is needed. `/widget/state` also includes `pending_operations` and an `ETag`; the loader uses both to poll efficiently and refresh every mounted widget on the page. ## Host API ```ts await window.__HATCHED_WIDGET__.track('lesson_completed', { lessonId: 'lesson_1', }); ``` The loader reconciles effects into every mounted widget on the page. --- # Badges widget > A shelf of the buddy's earned and locked badges. Source: https://docs.hatched.live/docs/reference/widgets/badges ## Mount ```html
``` ## Required scopes - `read` Badges are included in the shared `/widget/state` snapshot, so the widget stays in sync with the buddy widget without a second token. The dedicated `GET /widget/badges` endpoint (earned + locked next-up tiles) is capability-gated, so the 403s below apply when that endpoint is called. ## Plan & capability - Capability: `badges` (tenant toggle in Settings → Capabilities). - Minimum plan: **Free**. - Two distinct 403s can come back: - `403 plan_feature_locked` — the plan does not entitle the capability. Details carry `required_plan` and an `upgrade_url`. - `403 capability_disabled` — the plan entitles it but an operator turned the Settings toggle off for this workspace. --- # Streak widget > A compact streak counter with milestones and current progress. Source: https://docs.hatched.live/docs/reference/widgets/streak ## Mount ```html
``` The loader mounts every `data-hatched-mount="streak"` element, so one page can render multiple streaks. Copy the `data-streak-key` value from the streak definition in Dashboard → Streaks. ## Display modes A streak definition picks one of three render modes (Dashboard → Streaks → Display mode): | Mode | What it renders | | ------- | ------------------------------------------------------------------------------- | | `count` | A hero icon plus a large `N ×` counter and a status sub-line. The default. | | `row` | `N` icons in a row (up to `max_row_icons`), then a `+overflow` chip and sub-line. | | `mini` | A bare inline `🔥 N` chip — no label, sub-line, or attribution. Built to sit inside a host navbar or menu item. | A single mount can override the definition's mode with `data-display-mode`, so the same streak can render `count` on a dashboard page and `mini` in the top nav without a second definition: ```html ``` The `mini` chip renders at the widget's base type scale (~14px); set `font-size` on the mount element to resize the whole chip. The flame tints to the widget accent only while the streak is active for the current period — otherwise it stays muted. ## Mount attributes | Attribute | Values | Default | | ------------------- | ---------------------------- | ---------------------- | | `data-streak-key` | streak definition key | required | | `data-display-mode` | `count` · `row` · `mini` | the definition's mode | ## Plan & capability - Capability: `streaks` (tenant toggle in Settings → Capabilities). - Minimum plan: **Free**. - Two distinct 403s can come back: - `403 plan_feature_locked` — the plan does not entitle the capability. Details carry `required_plan` and an `upgrade_url`. - `403 capability_disabled` — the plan entitles it but an operator turned the Settings toggle off for this workspace. ## Required scopes - `read` --- # Path widget > A guided journey of steps and sub-steps, rendered as a straight column, a zigzag quest, or a horizontal stepper. Source: https://docs.hatched.live/docs/reference/widgets/path ## Mount ```html
``` The loader mounts every `data-hatched-mount="path"` element. Each mount hits `/widget/path[/]` once on init and never re-fetches — further state updates ride on `effects.path_updates` after `track()` round-trips. ## Mount attributes | Attribute | Values | Default | | ---------------- | ----------------------------------------------- | -------- | | `data-path-key` | A specific path definition key | optional | When `data-path-key` is omitted, the server returns the audience's currently-active path. When set, the widget renders that path even if it is no longer active — useful for previewing a draft. ## Display modes The widget renders either layout based on the definition's `display_mode` field. Operators flip the mode from the dashboard; the host page does not pick. - **`straight`** (Straight column) — readable vertical path with a continuous accent connector. Tap a node to expand its sub-steps inline. - **`zigzag`** (Zigzag quest) — alternating nodes and labels, Duolingo-style. Tap a node to expand its sub-steps inline. - **`stepper`** (Compact stepper) — horizontal scrolling chip row with the active step's sub-steps expanded inline below. ## Path icon The definition's `icon` field renders optional decoration next to the path label. Allowed values are `path`, `flame`, `heart`, `bolt`, `star`, and `leaf`; `path` means no icon. The API rejects other values. ## Sub-step CTAs Each sub-step can render up to two interactive elements: - An external link, when `content_url` is set, labelled with `cta_label` (or "Open" by default). Opens in a new tab. - A "Mark complete" button, when `allow_manual_complete: true` *and* the current token has the `events:track` scope. Embed-token mounts (read-only) hide the button. Sub-steps with neither will be rendered as a labelled list item with no actions — operators usually pair `content_url` with manual completion or with an automatic `completion_condition`. ## Required scopes - `read` — to load the path payload. - `events:track` — only if the page wants the manual-complete CTA. Embed tokens with `read` only still render the path; the CTA is silently hidden. ## Plan & capability Unlike most widgets, Path is gated by the `paths` runtime capability, and that capability requires a **Pro plan or higher**. Every path endpoint (`GET /widget/path`, `GET /widget/path/`, and the manual-complete `POST`) is wrapped in this gate, so a tenant that is not entitled will get a `403` on the very first mount. Two distinct `403` responses can come back: - **`plan_feature_locked`** — the workspace is on Free or Growth, which do not include `paths`. Upgrade to Pro or Enterprise to unlock it. - **`capability_disabled`** — the workspace is on Pro/Enterprise but the `paths` capability has been turned off in **Settings → Capabilities**. Re-enable the toggle and the endpoints start serving again. ## Empty state When the audience has no active path, the widget renders "No active path yet" instead of spinning. Embedders can use this to verify the mount worked even before the operator activates a path. If `data-path-key` points to a path that does not exist for the buddy's audience, the API returns 404 and the widget renders the error state. ## Theming The widget honours the customer brand theme by default. To override the accent for a specific path, set `accent_color` on the definition — the widget exposes it as `--hw-path-accent`. The eight-axis personality system from the workspace theme (surface, geometry, motion profile, …) is forwarded as `data-*` attributes on the size wrapper. --- # Tokens widget > The buddy's wallet — the spendable primary balance plus every progression token, each with progress toward its gate. Source: https://docs.hatched.live/docs/reference/widgets/tokens ## Mount ```html
``` The loader mounts every `data-hatched-mount="tokens"` element. Each mount hits `/widget/tokens` once on init. There are no per-mount attributes — the wallet is scoped to the buddy carried by the token. ## What it renders - **Primary balance** — the spendable currency (coins, or whatever the customer renamed it to in the dashboard), shown as the headline number. - **Progression tokens** — one row per active progression token (the earn-only gate currency, XP-like). Each row shows the token's label, a glyph, the current balance, and — when the token defines a `max_balance` — a progress bar toward it (`7 / 10 Mastery Stars`). Tokens without a cap render the balance alone. Customers without a progression token get a clean single-balance card. See [Tokens](/docs/concepts/tokens) for the two-tier model behind this. ## Liveness The primary number stays live: it is read from the shared `/widget/state` snapshot (`buddy.coins`), so `hatched.track()` round-trips update it without re-fetching `/widget/tokens`. Progression balances update from `effects.tokens` returned by `track()` (format `token_key:amount`), so earning a progression token reflects in the wallet immediately. A token that did not exist in the wallet at mount time appears after the next remount. ## Required capability and scopes - Capability: `tokens` must be enabled for the customer. If it is off the endpoint returns 403 and the widget renders the error state. - Scope: `read` — to load the wallet. No write scope is needed; the tokens widget is read-only (spending happens through gates, not here). ## Theming The widget honours the customer brand theme. The eight-axis personality system from the workspace theme (surface, geometry, motion profile, reward voice, iconography, typography) is forwarded as `data-*` attributes on the size wrapper, exactly like the other widgets. --- # Leaderboard widget > Ranked list surface for the buddy's audience. Source: https://docs.hatched.live/docs/reference/widgets/leaderboard ## Mount ```html
``` ## Required scopes - `read` Leaderboards are read-only in the widget runtime. Use your server-side SDK for admin configuration and metric ingestion. --- # Marketplace widget > Browse items, purchase with coins, and equip items on the buddy. Source: https://docs.hatched.live/docs/reference/widgets/marketplace ## Mount ```html
``` ## Required scopes - `read` renders catalog, ownership, and affordability. - `marketplace:browse` documents browse intent for session review, but browse itself is read-only. - `marketplace:purchase` enables purchases. - `items:equip` enables equip/unequip. Read-only embed tokens can display state, but buying and equipping require a widget session token with the mutation scopes above. ## Appearance updates Equip and unequip actions render a new buddy image. The action can complete from cache immediately or return an operation id while `buddy.appearance.status` is `pending`. During that window the marketplace disables further outfit changes and shows an appearance banner. Possible statuses: - `ready` — current `image_url` reflects the rendered outfit. - `pending` — an image composite is running. - `awaiting_credits` — Hatched will retry after image credits are available. - `failed` — the user or operator must retry or rerender the base. The widget reads `scopes` from `/widget/state`. If the token lacks `marketplace:purchase` or `items:equip`, the catalog remains visible but mutating actions are disabled. ## Rerender recovery When `appearance.status === 'failed'` and `appearance.error.code === 'needs_rerender'`, the buddy needs a clean bare stage before items can change. Use a session token with `items:equip` and call: ```http POST /api/v1/widget/appearance/rerender Authorization: Bearer WIDGET_SESSION_TOKEN ``` After the rerender operation returns `ready`, re-equip the desired items. Rerender clears the rendered item set so the UI never claims an item is visible when the current image is bare. --- # Kudos widget > Tenant-defined peer recognition — send and receive kudos with a configurable taxonomy. Source: https://docs.hatched.live/docs/reference/widgets/kudos ## Mount ```html
``` Sending a kudos is a write, so the Kudos widget must be mounted with a **session token** issued server-side per user. With a `data-embed-token` the received/given history still renders, but the composer is disabled. ## Script attributes | Attribute | Values | Default | | -------------------- | ---------------------- | --------------------------------- | | `data-session-token` | widget session token | composer enabled | | `data-embed-token` | embed token | read-only history | | `data-theme` | `light` `dark` | `light` | | `data-api-base-url` | API origin + `/api/v1` | `https://api.hatched.live/api/v1` | ## Required scopes - `read` lists received and given kudos. - Sending a kudos requires a **session token** minted with the `kudos:send` scope; a session token without `kudos:send` is rejected `403`, and embed tokens are read-only. ## Plan & capability - Capability: `kudos` (tenant toggle in Settings → Capabilities). - Minimum plan: **Free**. - Two distinct 403s can come back: - `403 plan_feature_locked` — the plan does not entitle the capability. Details carry `required_plan` and an `upgrade_url`. - `403 capability_disabled` — the plan entitles it but an operator turned the Settings toggle off for this workspace. ## Endpoints | Method | Path | Purpose | | ------ | ----------------------- | --------------------------------------------------- | | `GET` | `/widget/kudos/types` | Effective kudo taxonomy for the composer | | `POST` | `/widget/kudos` | Send a kudos (`to_buddy_id`, `kudo_type_key`, `message?`) | | `GET` | `/widget/kudos/received`| Recent received kudos (Trophy Shelf) | | `GET` | `/widget/kudos/given` | Recent given kudos + lifetime count | When the taxonomy is empty the API returns the generic virtual set (`thanks` / `shoutout` / `support`). Sending is rate-limited by a workspace daily cap (`429`) and de-duplicated inside a 60s accident-click window. --- # Group Quest widget > Cooperative team goal with a shared progress bar — join, contribute, and win together. Source: https://docs.hatched.live/docs/reference/widgets/group-quest ## Mount ```html
``` Joining or leaving a quest is a write, so mount the Group Quest widget with a **session token**. With an embed token the active quests render read-only and the join button is disabled. ## Script attributes | Attribute | Values | Default | | -------------------- | ---------------------- | --------------------------------- | | `data-session-token` | widget session token | join enabled | | `data-embed-token` | embed token | read-only | | `data-theme` | `light` `dark` | `light` | | `data-api-base-url` | API origin + `/api/v1` | `https://api.hatched.live/api/v1` | ## Required scopes - `read` lists the active quests visible to the buddy. - `quests:join` is required to join or leave — mount with a **session token** granted the `quests:join` scope. Embed tokens are read-only. ## Plan & capability - Capability: `group_quest` (tenant toggle in Settings → Capabilities). - Minimum plan: **Growth**. - Two distinct 403s can come back: - `403 plan_feature_locked` — the plan does not entitle the capability. Details carry `required_plan` and an `upgrade_url`. - `403 capability_disabled` — the plan entitles it but an operator turned the Settings toggle off for this workspace. ## Endpoints | Method | Path | Purpose | | ------ | ------------------------------- | -------------------------------------------------- | | `GET` | `/widget/group-quests/active` | Active quests visible to the buddy (progress, deadline) | | `POST` | `/widget/group-quests/:id/join` | Join a quest — idempotent (`already_joined` on re-join) | | `POST` | `/widget/group-quests/:id/leave`| Leave a quest — prior contribution stays counted | > The public mount name is `group-quest`; the API routes are namespaced under > `/widget/group-quests` (plural). --- # Feed widget > Passive team activity feed (SeeSaw Bump) — celebrate teammates' milestones with a clap. Source: https://docs.hatched.live/docs/reference/widgets/feed ## Mount ```html
``` The feed itself is read-only and works with an embed token. Reacting (👏 clap) is a write, so swap in a `data-session-token` to enable the clap toggle. ## Script attributes | Attribute | Values | Default | | -------------------- | ---------------------- | --------------------------------- | | `data-embed-token` | embed token | read-only feed | | `data-session-token` | widget session token | clap enabled | | `data-theme` | `light` `dark` | `light` | | `data-api-base-url` | API origin + `/api/v1` | `https://api.hatched.live/api/v1` | ## Required scopes - `read` renders the cursor-paginated team feed. - `feed:react` is required to clap — the session token must be granted this scope. Embed tokens are read-only. ## Plan & capability - Capability: `seesaw_bump` (tenant toggle in Settings → Capabilities). - Minimum plan: **Free**. - Two distinct 403s can come back: - `403 plan_feature_locked` — the plan does not entitle the capability. Details carry `required_plan` and an `upgrade_url`. - `403 capability_disabled` — the plan entitles it but an operator turned the Settings toggle off for this workspace. ## Endpoints | Method | Path | Purpose | | ------ | ----------------------------------- | ---------------------------------------------- | | `GET` | `/widget/feed/team-events` | Team + customer-wide events, newest first (cursor) | | `POST` | `/widget/feed/team-events/:id/clap` | Toggle a 👏 clap (idempotent) | Clapping your own event is rejected with `400 self_clap_forbidden`. A fresh clap notifies the subject over the `feed.team_event` channel. --- # Mystery Box widget > Once-a-day surprise reward — a weighted-random coin drop with a deterministic daily seed. Source: https://docs.hatched.live/docs/reference/widgets/mystery-box ## Mount ```html
``` Opening the box is a write, so the Mystery Box widget must be mounted with a **session token**. The `state` endpoint is readable with an embed token so the widget can render its eligible / capped / locked face. ## Script attributes | Attribute | Values | Default | | -------------------- | ---------------------- | --------------------------------- | | `data-session-token` | widget session token | claim enabled | | `data-embed-token` | embed token | read-only state | | `data-theme` | `light` `dark` | `light` | | `data-api-base-url` | API origin + `/api/v1` | `https://api.hatched.live/api/v1` | ## Required scopes - `read` renders the box state. - Claiming requires a **session token** minted with the `mysterybox:claim` scope; a session token without `mysterybox:claim` is rejected `403`, and embed tokens are read-only. ## Plan & capability - Capability: `mystery_box` (tenant toggle in Settings → Capabilities). - Minimum plan: **Growth**. - Two distinct 403s can come back on both `/state` and `/claim` — the capability is gated at the controller level, so the guard rejects before the handler runs: - `403 plan_feature_locked` — the plan does not entitle the capability. Details carry `required_plan` and an `upgrade_url`. - `403 capability_disabled` — the plan entitles it but an operator turned the Settings toggle off for this workspace. The `{ locked: true }` state is the entitled-but-unavailable face, not the not-entitled response: when the plan does not entitle `mystery_box`, both `/state` and `/claim` return `403 plan_feature_locked` before that branch runs. ## Endpoints | Method | Path | Purpose | | ------ | -------------------------- | ------------------------------------------------ | | `GET` | `/widget/mystery-box/state`| `locked` / `eligible` / `next_eligible_at` / last reward | | `POST` | `/widget/mystery-box/claim`| Open the box — `409 mystery_box_daily_cap` once spent | The daily cap is one open per buddy and resets at midnight UTC. The reward draw is deterministically seeded per `(buddy, UTC day)`, so it can never be re-rolled. --- # League widget > Seasonal tier standing — live cohort board, promotion target, and Boss Fight progress. Source: https://docs.hatched.live/docs/reference/widgets/league ## Mount ```html
``` The League widget is read-only, so an embed token is enough. ## Script attributes | Attribute | Values | Default | | -------------------- | ---------------------- | --------------------------------- | | `data-embed-token` | embed token | read-only | | `data-session-token` | widget session token | read-only | | `data-theme` | `light` `dark` | `light` | | `data-api-base-url` | API origin + `/api/v1` | `https://api.hatched.live/api/v1` | ## Required scopes - `read` renders the tier banner, cohort standings and Boss Fight progress. ## Plan & capability - Capability: `leagues` (plan entitlement + a tenant toggle in Settings → Capabilities; on by default). - Minimum plan: **Growth**. The capability is enforced at the controller, so: - When the plan does not entitle LEAGUES the endpoints return `403 plan_feature_locked`. - When the tenant has switched the `leagues` toggle off the endpoints return `403 capability_disabled`. - When entitled and enabled but no season is running (or the buddy is not enrolled) the endpoints return an `available: false` snapshot and the widget renders nothing. ## Endpoints | Method | Path | Purpose | | ------ | --------------------------- | ------------------------------------------------ | | `GET` | `/widget/leagues/me` | Live tier, cohort standings, promotion target, demotion flag | | `GET` | `/widget/leagues/boss-fight`| Season Boss Fight challenge — progress, target, leaderboard | --- # Council widget > Elite-circle surface — Council members co-author user-facing narrative copy. Source: https://docs.hatched.live/docs/reference/widgets/council ## Mount ```html
``` Reading your own proposals and Council standing works with an embed token. Submitting a proposal is a write and is restricted to Council members, so the composer requires a **session token**. ## Script attributes | Attribute | Values | Default | | -------------------- | ---------------------- | --------------------------------- | | `data-session-token` | widget session token | composer enabled | | `data-embed-token` | embed token | read-only standing | | `data-theme` | `light` `dark` | `light` | | `data-api-base-url` | API origin + `/api/v1` | `https://api.hatched.live/api/v1` | ## Required scopes - `read` renders the buddy's proposals, Council standing and quota. - Submitting a proposal requires a **session token** granted the `council:propose` scope. A session token minted without it (for example `["read", "events:track"]`) is rejected with `403`. ## Plan & capability - Capability: `council` (tenant toggle in **Settings → Capabilities**, on by default). When an Enterprise operator turns it off the widget endpoints return `403` `capability_disabled`. - Minimum plan: **Enterprise**. A non-Enterprise (un-entitled) tenant gets a `403` `plan_feature_locked` before any membership check runs — it never returns a `200` locked state. - Membership is the gate on top of the plan: for an Enterprise tenant whose buddy is not a Council member, `GET /widget/council/proposals/mine` returns `200` with `is_council_member: false` so the widget renders its locked state. ## Endpoints | Method | Path | Purpose | | ------ | ------------------------------- | ------------------------------------------------ | | `GET` | `/widget/council/proposals/mine`| The buddy's proposals + Council standing + quota | | `POST` | `/widget/council/proposals` | Submit a narrative proposal (members only) | --- # Hexad Survey widget > Marczewski Hexad player-type survey — 24 questions that derive a player's primary type. Source: https://docs.hatched.live/docs/reference/widgets/hexad-survey ## Mount ```html
``` Submitting (or deleting) a response is a write, so mount the survey with a **session token**. The buddy never passes its own IDs — the widget context resolves the buddy, customer and audience from the token. ## Script attributes | Attribute | Values | Default | | -------------------- | ---------------------- | --------------------------------- | | `data-session-token` | widget session token | submit enabled | | `data-embed-token` | embed token | read-only | | `data-theme` | `light` `dark` | `light` | | `data-api-base-url` | API origin + `/api/v1` | `https://api.hatched.live/api/v1` | ## Required scopes - `read` lists the question metadata and the buddy's own response. - Submitting a response requires a **session token** granted the `survey:submit` scope — `POST .../responses` is rejected `403` without it. - `DELETE .../me` requires no write scope; it is always permitted (see **Availability**), so `survey:submit` applies only to submit. ## Plan & capability - No plan capability gate — availability is controlled per workspace by the survey configuration (Audience → Survey settings) and the buddy's role. - Minimum plan: **Free**. ## Availability The survey is gated per workspace: the reads (`GET .../questions`, `GET .../me`) and the write (`POST .../responses`) all require the survey to be both enabled and widget-visible. When it isn't, those endpoints return `403 hexad_survey_disabled`. `DELETE /widget/hexad-survey/me` is the one exception — it is **always permitted**, even when the survey is disabled, so a user can withdraw consent and erase their raw response at any time. ## Endpoints | Method | Path | Purpose | | -------- | ----------------------------- | ------------------------------------------------ | | `GET` | `/widget/hexad-survey/questions` | 24 question keys + axis assignment + consent version | | `POST` | `/widget/hexad-survey/responses` | Submit/replace the response (UPSERT by buddy) | | `GET` | `/widget/hexad-survey/me` | The buddy's current response (or `null`) | | `DELETE` | `/widget/hexad-survey/me` | Delete the raw response (GDPR / consent withdrawal) | Verbatim Marczewski question text ships in the widget's tenant-installable copy bundle, not from the API. Responses carry a `consent_version` and a `retention_expires_at`. --- # Login widget > Historical note for the retired login widget. Source: https://docs.hatched.live/docs/reference/widgets/login The current widget loader does not ship a login widget. Hatched assumes your product owns authentication, then your backend mints widget session tokens for the signed-in user. Use [Widget integration](/docs/guides/widget-integration) for the supported flow. --- # Celebrate host > One-shot celebration overlay for badges, evolutions, and streak milestones. Source: https://docs.hatched.live/docs/reference/widgets/celebrate Celebrations are exposed from the shared loader. Include the loader once, then call the host API when your product wants a moment. ```html ``` ```ts window.__HATCHED_WIDGET__.celebrate({ kind: 'badge', badge: { key: 'week_warrior', label: 'Week Warrior', description: 'Seven active days in a row', }, tier: 'bloom', }); ``` The celebrate bundle lazy-loads only when the first celebration is fired. --- # Theme tokens > Every CSS variable the widget loader honors — what it does, default value in light and dark mode, and how to override it from the script tag or at runtime. Source: https://docs.hatched.live/docs/reference/theme-tokens The widget loader exposes a single contract for visual customization: a flat set of `--hw-*` CSS custom properties. Every widget reads from these variables — recolor them once and every mounted widget picks up the new palette without touching the loader bundle. This page lists every variable the loader writes, how it maps to the underlying token system, and how to override it at three different levels. ## How overrides flow The loader resolves theme tokens in this order, last write wins: 1. **Built-in defaults** — `light` or `dark` palette baked into the loader. 2. **`data-theme-vars`** — JSON-encoded `--hw-*` map on the script tag (set once when you paste the embed). 3. **`window.HatchedWidgets.remount({ themeVars })`** — runtime overrides pushed from your host page after the embed has mounted. See the [Runtime configuration](/docs/guides/widget-integration#runtime-configuration) guide for details. Any key that does **not** begin with `--hw-` is dropped by the loader's style serializer, so you can't accidentally leak host-page CSS variables into the shadow DOM. Pass strings only; the loader does not parse JS values. ## Try it The playground writes overrides into a mock widget styled with the same tokens as the production loader. Adjust a token to see exactly which surface it controls, then copy the script tag at the bottom into your real embed. ## Reference ### Surfaces | Variable | Light | Dark | What it controls | | --- | --- | --- | --- | | `--hw-bg` | `#FAF7F2` | `#0D0B08` | Outer widget shell background | | `--hw-bg-elevated` | `#FEFCF8` | `#1A1814` | Elevated panels (popovers, sheets) | | `--hw-surface` | `#FFFFFF` | `#2A241A` | Card backgrounds inside the shell | | `--hw-surface-muted` | `#F4EFE6` | `#1A1814` | Secondary chip / neutral pill background | | `--hw-overlay` | `rgba(13,11,8,0.48)` | `rgba(0,0,0,0.72)` | Modal scrim | ### Borders | Variable | Light | Dark | What it controls | | --- | --- | --- | --- | | `--hw-border` | `#E0D7C3` | `#3D3629` | Default card and shell border | | `--hw-border-strong` | `#CFC9BE` | `#5A5246` | Hover/active borders | ### Text | Variable | Light | Dark | What it controls | | --- | --- | --- | --- | | `--hw-text` | `#1A1814` | `#FEFCF8` | Default body text | | `--hw-text-secondary` | `#5A5246` | `#ECE5D6` | Sub-headings, secondary labels | | `--hw-text-muted` | `#7E7668` | `#A8A196` | Meta text, captions | | `--hw-on-accent` | `#FFFFFF` | `#0D0B08` | Text drawn on top of `--hw-accent` | ### Accent (primary brand color) The accent ramp is what most tenants override first. Pick a brand hue for `--hw-accent`, derive a darker `--hw-accent-strong` for hover and a tinted `--hw-accent-soft` for chips/pills. | Variable | Light | Dark | What it controls | | --- | --- | --- | --- | | `--hw-accent` | `#FF6B4A` | `#FF8562` | Primary buttons, key chips, progress fills | | `--hw-accent-strong` | `#E84F30` | `#FFA284` | Hover, focus, active states | | `--hw-accent-soft` | `#FFE4D8` | `rgba(255,133,98,0.16)` | Soft fills, badge backgrounds, ghost rows | ### Semantic colors | Variable | Light | Dark | What it controls | | --- | --- | --- | --- | | `--hw-positive` | `#12E8A0` | `#5FFFC6` | Success states, completed progress | | `--hw-positive-soft` | `#C9FFEC` | `rgba(45,255,185,0.16)` | Success chip backgrounds | | `--hw-info` | `#6E7BFF` | `#8E99FF` | Informational toasts and tags | | `--hw-warn` | `#F5A800` | `#FFC427` | Warning ribbons, "at risk" cues | | `--hw-danger` | `#F84C5C` | `#FF7C87` | Destructive actions, error toasts | | `--hw-yolk` | `#FFC427` | `#FFC427` | The Hatched egg yolk (rarely overridden) | ### Geometry | Variable | Default | What it controls | | --- | --- | --- | | `--hw-radius` | `16px` | Outer shell radius | | `--hw-card-radius` | `12px` | Card / panel radius | | `--hw-button-radius` | `8px` | Buttons, chips | | `--hw-density-scale` | `1` | Multiplier on vertical rhythm (try `0.85` for compact, `1.15` for relaxed) | | `--hw-pad-shell` | `16px` | Shell padding | | `--hw-pad-card` | `12px` | Card padding | | `--hw-gap` | `12px` | Default flex/grid gap inside widgets | ### Typography The loader ships **Geist** for body copy and **Gluten** for the playful display family by default. Override these to inherit the host site's fonts — the loader injects the fallback chain you provide; it does not bundle additional webfonts. | Variable | Default | What it controls | | --- | --- | --- | | `--hw-font-body` | `'Geist', 'Inter', system-ui, sans-serif` | Body text | | `--hw-font-display` | `'Gluten', 'DynaPuff', 'Fraunces', serif` | Headlines, ceremony copy | | `--hw-font-mono` | `'Geist Mono', 'JetBrains Mono', ui-monospace, monospace` | Code, tokens, numeric counters | | `--hw-font-base` | `14px` | Body size | | `--hw-font-title` | `20px` | Section headings | | `--hw-font-meta` | `12px` | Caption text | | `--hw-font-eyebrow` | `10px` | Uppercase eyebrows | ### Other geometry primitives | Variable | Default | What it controls | | --- | --- | --- | | `--hw-avatar` | `64px` | Avatar / buddy portrait square | | `--hw-tile-min` | `140px` | Minimum tile width in marketplace / grid widgets | | `--hw-bar-h` | `8px` | Progress bar height | | `--hw-btn-pad-y` | `8px` | Button vertical padding | | `--hw-btn-pad-x` | `16px` | Button horizontal padding | | `--hw-shell-max` | `360px` | Widest the shell will grow before clamping | | `--hw-shadow` | `0 1px 2px rgba(13,11,8,0.04), 0 1px 1px rgba(13,11,8,0.02)` | Card shadow | ### Alias tokens (you don't need to set these) A handful of widgets reference parallel token names that resolve to the canonical tokens above. **You never have to set them** — override the canonical token and the alias follows automatically. They exist only so older widget styles keep working; listing them here so nothing looks "missing". | Alias | Resolves to | | --- | --- | | `--hw-color-primary` / `--hw-color-primary-strong` / `--hw-color-primary-soft` | `--hw-accent` / `--hw-accent-strong` / `--hw-accent-soft` | | `--hw-text-primary` / `--hw-text-default` | `--hw-text` | | `--hw-border-default` / `--hw-border-subtle` | `--hw-border` | | `--hw-path-accent` | `--hw-accent` (Path widget node ramp) | | `--hw-radius-card` / `--hw-radius-button` | `--hw-card-radius` / `--hw-button-radius` | | `--hw-color-text-inverse` / `--hw-color-surface-inverse` | `--hw-bg` / `--hw-text` | ### Optional overrides (no loader default) These tokens are **not** written by the loader's `:host` block, so they have no Light/Dark default of their own. They are only read as the first choice in a `var()` fallback chain — leave them unset and the listed fallback applies; set them with `data-theme-vars` (or `remount`) to take control of that one surface. | Variable | Falls back to | What it controls | | --- | --- | --- | | `--hw-bg-sunken` | `--hw-bg` | Inset wells (progress trays, code blocks) | | `--hw-shell` | `#FEFCF8` (literal) | Chrome layering used by the dressing animation SVG | | `--hw-skeleton-base` | `--hw-bg-sunken`, then `--hw-bg` | Skeleton placeholder base | | `--hw-skeleton-shine` | `--hw-bg-elevated`, then `--hw-bg` | Skeleton shimmer highlight | ## Setting tokens from the script tag The simplest path: pass a `data-theme-vars` attribute on the loader script. The value is JSON, keys are CSS variable names, values are strings. ```html
``` Keys without the `--hw-` prefix are dropped on parse — there is no way to leak unrelated host-page variables. ## Setting tokens at runtime When the host application is an SPA and the embed has already mounted, push new tokens through the public API: ```ts window.HatchedWidgets.remount({ themeVars: { '--hw-accent': '#5B5BFF', '--hw-accent-strong': '#3F3FFF', '--hw-accent-soft': '#E4E4FF', }, }); ``` `remount()` re-runs mount discovery and updates the in-memory config — already-mounted widgets receive the new tokens via their shadow-DOM style elements, no flicker. ## Tips - **Light + dark with one accent ramp.** Override `--hw-accent`, `--hw-accent-strong`, and `--hw-accent-soft` once with `data-theme-vars` and the loader keeps the rest of the palette in sync with `data-theme`. - **Densify a sidebar embed.** Set `--hw-density-scale: 0.85` to drop the vertical rhythm without rewriting every widget's spacing. - **Match host fonts.** Override `--hw-font-body` and `--hw-font-display` with the host site's fallback chain. The loader does not load additional webfonts — the host page is responsible for serving them. - **Round buttons but square cards.** Tokens are independent: pick `--hw-button-radius: 9999px` for pill buttons while keeping `--hw-card-radius: 8px`. ## Companion docs - [Widget integration guide](/docs/guides/widget-integration) — how to paste the embed, choose a mode, and respond to events. - [Widget reference](/docs/reference/widgets/buddy) — every widget and the `data-*` attributes it accepts. - [`@hatched/widgets-loader-types`](https://www.npmjs.com/package/@hatched/widgets-loader-types) — IDE autocomplete for `themeVars` and `window.HatchedWidgets`. --- # Webhook payloads > Common event payloads Hatched emits, with the shape of the body you'll receive. Source: https://docs.hatched.live/docs/reference/webhook-payloads There is no wrapping envelope. The POST body **is** the raw event payload — the per-type object shown below, with `snake_case` keys. Event metadata travels only in HTTP headers, never in the body. Headers on every delivery: ``` Content-Type: application/json X-Hatched-Event: badge.awarded X-Hatched-Delivery: X-Hatched-Timestamp: X-Hatched-Signature: sha256= ``` - `X-Hatched-Event` — the event name (e.g. `badge.awarded`). - `X-Hatched-Delivery` — the delivery id. **This is the dedupe key** — there is no `deliveryId` in the body. - `X-Hatched-Timestamp` — its own header (unix seconds), not embedded in the signature. - `X-Hatched-Signature` — `sha256=`, where the hex is `HMAC-SHA256` of `` `${timestamp}.${rawBody}` ``. Verify with `WebhooksResource.verifySignature` from `@hatched/sdk-js` — see [Handle webhooks](/docs/guides/handle-webhooks). ## Event types The shapes below cover the most commonly subscribed events. They are not the full catalog — Hatched emits many more (mystery box, lottery, league, kudos, group quest, booster, and others). The canonical, always-current list of every event name is served by `GET /webhook-configs/events`; treat that endpoint as the source of truth for what you can subscribe to. ### buddy.hatched Emitted when an egg's hatch operation completes. ```json { "buddy_id": "buddy_01…", "egg_id": "egg_01…", "user_id": "user_42", "name": "Pip", "image_url": "https://cdn.hatched.live/…", "thumb_url": "https://cdn.hatched.live/…", "evolution_stage": 1 } ``` ### buddy.evolved ```json { "buddy_id": "buddy_01…", "previous_stage": 1, "new_stage": 2, "image_url": "https://cdn.hatched.live/…" } ``` ### coins.earned / coins.spent ```json { "buddy_id": "buddy_01…", "amount": 10, "balance": 120, "reason": "lesson_completed", "reference_id": "ref_01…", "ledger_id": "ledger_01…", "is_lucky": false } ``` ### token.earned / token.spent ```json { "buddy_id": "buddy_01…", "token_type": "gem", "amount": 1, "reason": "rule:weekly_quiz_passed" } ``` ### gate.unlocked Fires when a buddy spends tokens to unlock a token gate. ```json { "buddy_id": "buddy_01…", "gate_key": "premium_lessons", "token_key": "gem", "cost": 5, "unlocked_at": "2026-06-01T12:00:00.000Z" } ``` ### skill.level_up ```json { "buddy_id": "buddy_01…", "skill_key": "pronunciation", "level": 3, "previous": 2, "change": 1 } ``` ### skill.updated Fires whenever a buddy's skill value changes — including rule-engine reward paths. Use this when you want every skill movement rather than only the threshold crossings that `skill.level_up` reports. ```json { "buddy_id": "buddy_01…", "updates": { "vocabulary": { "level": 78, "previous": 80, "change": -2 } } } ``` ### skill.decayed Fires when a skill-decay rule lowers (or refreshes) a buddy's skill level for a decay period. A drop also emits a paired `skill.updated`. ```json { "buddy_id": "buddy_01…", "user_id": "user_123", "skill_key": "vocabulary", "previous_level": 80, "new_level": 78, "delta": -2, "rule_id": "sdr_01…", "period_key": "2026-W22" } ``` ### badge.ready / badge.awarded `badge.ready` fires for manual badges awaiting review; `badge.awarded` fires when the badge actually attaches. `badge.ready` carries only `{ buddy_id, badge_key }`; `badge.awarded` carries the full shape below. ```json { "buddy_id": "buddy_01…", "badge_key": "week_warrior", "label": "Week Warrior", "awarded_at": "2026-04-22T10:30:00Z", "coin_reward": 50, "reason": null } ``` ### streak.milestone ```json { "buddy_id": "buddy_01…", "current_streak": 7, "longest_streak": 12, "milestone": 7, "event_id": "evt_01…" } ``` `streak.milestone` only fires on the configured thresholds. To react when a streak is about to lapse, subscribe to `streak.at_risk`. ### item.purchased / item.equipped ```json { "buddy_id": "buddy_01…", "item_id": "item_cowboy_hat", "item_name": "Cowboy Hat", "price_paid": 50, "purchase_id": "purchase_01…" } ``` ### evolution.ready ```json { "buddy_id": "buddy_01…", "current_stage": 1, "next_stage": 2, "progress": 1, "auto_evolve": false } ``` ### buddy.prestiged Fires when a buddy crosses the Prestige threshold and resets its evolution stage with the prestige counter incremented. The buddy keeps its history; the public share page picks up a prestige aura automatically. ```json { "buddy_id": "buddy_01…", "prestige_level": 1, "from_evolution_stage": 5, "season_id": "season_2026q2", "occurred_at": "2026-05-25T10:30:00Z" } ``` ### team_membership.role_upgraded Fires when a buddy is granted a higher role within a Team — currently the only upgrade path is to `mentor` (after meeting the configured mentor-hour threshold). Use this to surface a "you're now a mentor" notification in your product. ```json { "buddy_id": "buddy_01…", "from_role": "member", "to_role": "mentor", "occurred_at": "2026-05-25T10:30:00Z" } ``` ### cause.threshold_reached Symbolic Humanity Hero counter crossed a configured unit threshold (e.g. every 100 trees planted, every 1,000 meals served). Unlike other webhook types, **this event delivers to the cause's own `webhook_url`** — configured per-cause in the dashboard — rather than the customer-wide endpoints. Tenants typically wire this to a charity API or internal reporting service. ```json { "event": "cause.threshold_reached", "cause_id": "cause_01…", "cause_key": "trees_planted", "customer_id": "cus_01…", "total_units": 1300, "threshold_unit": 1300, "is_test": false, "occurred_at": "2026-05-25T10:30:00Z" } ``` Unlike the other events, the cause payload **embeds the event name and timestamp in the body** (`event` + `occurred_at`) rather than relying solely on the headers — this is a deliberate distinct shape, kept for compatibility with charity/reporting consumers. The delivery is still HMAC-signed the same way as every other webhook (same headers, same signature). `is_test` is true when the dashboard's "Send test delivery" button triggers the call. ### council.proposal_approved Fires when a Council proposal (UGC narrative line for a level-up slot) is approved by the configured moderator quorum. Use this to ship the new copy to your product — for example, refreshing localized strings in your CMS or re-rendering cached share pages. ```json { "customer_id": "cus_01…", "proposal_id": "council_prop_01…", "target_slot": "level_up_copy.stage_3", "occurred_at": "2026-05-25T10:30:00Z" } ``` `target_slot` is dot-notation into the customer's `narrative` JSONB — i.e. `level_up_copy.stage_3` points at the level-3 evolution flavor text. This lets a downstream system know exactly which surface to invalidate. ## Delivery semantics - **At-least-once.** Retries happen on non-2xx. Dedupe on the `X-Hatched-Delivery` header. - **No global ordering.** Events for the same buddy _tend_ to arrive in order but don't rely on it — carry your own sequence numbers in the payload if ordering matters. - **Replay window on your end.** Reject requests where the `X-Hatched-Timestamp` header is older than 5 minutes to defend against replay attacks. The SDK's `verifySignature` does this automatically when you pass the timestamp via `options.timestamp` (the framework adapters extract it for you). --- # Error codes > Every stable error code Hatched raises — HTTP status, SDK class, and how to fix. Source: https://docs.hatched.live/docs/reference/error-codes {/* AUTO-GENERATED — do not edit by hand. The set of codes is derived from the `ErrorCode` export in packages/sdk-js/src/error-codes.ts; descriptions and HTTP statuses come from the supplementary META map in apps/docs/scripts/generate-error-codes.ts. Regenerate with `pnpm --filter @hatched/docs generate:error-codes`. */} Every error response follows the canonical envelope: ```json { "error": { "code": "rate_limited", "message": "Rate limit exceeded. Retry after 60s", "requestId": "req_abc_123", "details": { "...": "..." } } } ``` The SDK parses this envelope and throws a typed subclass of `HatchedError` with `code`, `statusCode`, `requestId`, and `details`. The string codes match the `ErrorCode` enum exported from `@hatched/sdk-js`, so you can `switch` on `err.code` exhaustively. ## Catalogue | Code | HTTP | SDK class | Meaning | |---|---|---|---| | `bad_request` | 400 | `HatchedError` (base) | A generic domain bad-request thrown explicitly by a service or controller (e.g. a `from` date after `to`, an unparseable filter). Request-body/DTO validation does NOT land here — that is `validation_failed` (422). `err.details` shape varies per endpoint. | | `category_conflict` | 400 | `CategoryConflictError` | An equip request tried to put two items in the same category slot (only `accessory` stacks). `err.category` / `err.conflictingItemIds` are on the error. | | `insufficient_balance` | 400 | `InsufficientBalanceError` | Buddy does not have enough coins/tokens for the spend. | | `missing_audience` | 400 | `HatchedError` (base) | The customer has 2+ audiences defined, but the request omitted the `audience` field. Single-audience customers never see this (the server applies the implicit default). | | `too_many_items` | 400 | `TooManyItemsError` | An equip request asked the buddy to wear more items than the compositing pipeline can render (cap is four). `err.max` / `err.attempted` are on the error. | | `unknown_audience` | 400 | `HatchedError` (base) | The `audience` value sent on the request is not one of the customer’s configured audiences. | | `unauthorized` | 401 | `UnauthorizedError` | The API key is missing, invalid, or revoked. | | `credit_insufficient` | 402 | `CreditInsufficientError` | No credit pool has enough credits for the requested AI job. `err.required` / `err.available` / `err.topUpUrl` / `err.upgradeUrl` are on the error. | | `event_quota_exceeded` | 402 | `EventQuotaExceededError` | The monthly event quota for the plan is exhausted. `err.used` / `err.limit` / `err.resetAt` / `err.upgradeUrl` are on the error. | | `onboarding_cap_reached` | 402 | `HatchedError` (base) | A free onboarding/trial cap was hit (images, chat turns, or session duration). `err.details.cap` / `err.details.used` / `err.details.limit` / `err.details.upgrade_url` are on the error. | | `payment_required` | 402 | `HatchedError` (base) | Generic 402 fallback when a billing block has no more specific code (parsed as a base `HatchedError` with `code: "payment_required"`). | | `capability_disabled` | 403 | `HatchedError` (base) | A workspace-level capability flag for this feature is turned off, even though the plan would allow it. `err.details.feature` / `err.details.current_plan` are on the error. | | `forbidden` | 403 | `ForbiddenError` | Key is valid but lacks permission for this endpoint. | | `plan_feature_locked` | 403 | `PlanFeatureLockedError` | The customer's plan does not include the requested feature (e.g. Free tier hitting `/marketplace/*`). `err.requiredPlan` / `err.upgradeUrl` are on the error. | | `publishable_key_scope` | 403 | `PublishableKeyScopeError` | Publishable key cannot mutate — use a secret key server-side. | | `widget_token_scope` *(reserved)* | 403 | `WidgetTokenScopeError` | Reserved — not currently emitted by the API. The SDK defines `WidgetTokenScopeError` defensively for a future widget-token scope check. | | `not_found` | 404 | `NotFoundError` | Generic 404 fallback — a Nest route or guard threw a `NotFoundException` without the richer `resource_not_found` code (e.g. an unmatched route). | | `resource_not_found` | 404 | `NotFoundError` | The referenced id does not exist or was archived. | | `active_egg_limit` | 409 | `ActiveEggLimitError` | The per-user active-egg cap was reached. `err.details.active` lists the existing eggs (id + status). | | `config_version_mismatch` | 409 | `ConfigVersionMismatchError` | The buddy is pinned to a different config version than expected. | | `conflict` | 409 | `ConflictError` | A competing mutation won; retry is safe if you re-read state first. | | `idempotency_key_conflict` | 409 | `ConflictError` | An earlier request reused this `Idempotency-Key` with a different body. The SDK surfaces this as a generic `ConflictError` with `code: "idempotency_key_conflict"`. | | `no_published_config` | 409 | `NoPublishedConfigError` | The customer has no published config version yet, so eggs cannot be created. | | `validation_failed` | 422 | `ValidationError` | Request input failed validation (HTTP 422). Emitted for every validation path: class-validator DTO failures (via the global `ValidationPipe`), controllers that `Schema.parse(body)` with Zod, and domain business-rule checks. `err.details.fields` is a consistent `{ "": ["", ...] }` map across all three; Zod failures additionally include `err.details.issues` (the raw issue array). | | `rate_limited` | 429 | `RateLimitError` | Over the per-minute quota. Honour `err.retryAfter` (seconds). | | `internal_server_error` | 500 | `HatchedError` (base) | An unhandled error reached the global filter. The `message` is the thrown error message; no stable `details` shape. | | `bad_gateway` | 502 | `HatchedError` (base) | Generic 502 fallback when an upstream dependency failed without the more specific `upstream_image_error` code. | | `upstream_image_error` | 502 | `UpstreamImageError` | The art provider failed during hatch/evolve. No ledger writes happened. | | `onboarding_extract_failed` | 503 | `HatchedError` (base) | The onboarding pipeline could not verify the gamification selections extracted from the generated plan. No state was persisted. `err.details.reason` is present when a reason was captured. | | `service_unavailable` | 503 | `HatchedError` (base) | A dependency is temporarily unavailable (generic 503 fallback). | ## bad_request - **HTTP status:** 400 - **SDK class:** `HatchedError` (base) - **Meaning:** A generic domain bad-request thrown explicitly by a service or controller (e.g. a `from` date after `to`, an unparseable filter). Request-body/DTO validation does NOT land here — that is `validation_failed` (422). `err.details` shape varies per endpoint. - **Fix:** Read `err.message` / `err.details` to see which input was rejected; fix it and resend. - **More:** [Guide →](/docs/guides/troubleshooting#400-bad-request) ## category_conflict - **HTTP status:** 400 - **SDK class:** `CategoryConflictError` (from `@hatched/sdk-js`) - **Meaning:** An equip request tried to put two items in the same category slot (only `accessory` stacks). `err.category` / `err.conflictingItemIds` are on the error. - **Fix:** Unequip the conflicting item before equipping the new one. ## insufficient_balance - **HTTP status:** 400 - **SDK class:** `InsufficientBalanceError` (from `@hatched/sdk-js`) - **Meaning:** Buddy does not have enough coins/tokens for the spend. - **Fix:** Check `err.balance` / `err.required`; surface to the user. ## missing_audience - **HTTP status:** 400 - **SDK class:** `HatchedError` (base) - **Meaning:** The customer has 2+ audiences defined, but the request omitted the `audience` field. Single-audience customers never see this (the server applies the implicit default). - **Fix:** Send a valid `audience` (the role/segment key) on the request. List the configured audiences in Dashboard → Audiences. - **More:** [Guide →](/docs/concepts/audiences) ## too_many_items - **HTTP status:** 400 - **SDK class:** `TooManyItemsError` (from `@hatched/sdk-js`) - **Meaning:** An equip request asked the buddy to wear more items than the compositing pipeline can render (cap is four). `err.max` / `err.attempted` are on the error. - **Fix:** Unequip something first, then retry with at most `err.max` items. ## unknown_audience - **HTTP status:** 400 - **SDK class:** `HatchedError` (base) - **Meaning:** The `audience` value sent on the request is not one of the customer’s configured audiences. - **Fix:** Use one of the keys configured in Dashboard → Audiences (the `audience` must match exactly). - **More:** [Guide →](/docs/concepts/audiences) ## unauthorized - **HTTP status:** 401 - **SDK class:** `UnauthorizedError` (from `@hatched/sdk-js`) - **Meaning:** The API key is missing, invalid, or revoked. - **Fix:** Double-check `HATCHED_API_KEY`; rotate in Dashboard → Developers if leaked. - **More:** [Guide →](/docs/guides/troubleshooting#401-unauthorized) ## credit_insufficient - **HTTP status:** 402 - **SDK class:** `CreditInsufficientError` (from `@hatched/sdk-js`) - **Meaning:** No credit pool has enough credits for the requested AI job. `err.required` / `err.available` / `err.topUpUrl` / `err.upgradeUrl` are on the error. - **Fix:** Do NOT retry. Branch on `err.code`; send the user to `details.top_up_url` or `details.upgrade_url` (Stripe portal). - **More:** [Guide →](/docs/billing/handling-402) ## event_quota_exceeded - **HTTP status:** 402 - **SDK class:** `EventQuotaExceededError` (from `@hatched/sdk-js`) - **Meaning:** The monthly event quota for the plan is exhausted. `err.used` / `err.limit` / `err.resetAt` / `err.upgradeUrl` are on the error. - **Fix:** Do NOT retry. Branch on `err.code`; back off until `details.reset_at` (first of next UTC month) or show an upgrade prompt. - **More:** [Guide →](/docs/billing/handling-402) ## onboarding_cap_reached - **HTTP status:** 402 - **SDK class:** `HatchedError` (base) - **Meaning:** A free onboarding/trial cap was hit (images, chat turns, or session duration). `err.details.cap` / `err.details.used` / `err.details.limit` / `err.details.upgrade_url` are on the error. - **Fix:** Do NOT retry. Branch on `err.code`; send the operator to `details.upgrade_url` to lift the onboarding cap. - **More:** [Guide →](/docs/billing/handling-402) ## payment_required - **HTTP status:** 402 - **SDK class:** `HatchedError` (base) - **Meaning:** Generic 402 fallback when a billing block has no more specific code (parsed as a base `HatchedError` with `code: "payment_required"`). - **Fix:** Do NOT retry. Branch on `err.code`; surface upgrade / top-up to the operator. - **More:** [Guide →](/docs/billing/handling-402) ## capability_disabled - **HTTP status:** 403 - **SDK class:** `HatchedError` (base) - **Meaning:** A workspace-level capability flag for this feature is turned off, even though the plan would allow it. `err.details.feature` / `err.details.current_plan` are on the error. - **Fix:** Do NOT retry. Enable the capability for the workspace (Dashboard → Settings), or branch on `err.code` and hide the feature. ## forbidden - **HTTP status:** 403 - **SDK class:** `ForbiddenError` (from `@hatched/sdk-js`) - **Meaning:** Key is valid but lacks permission for this endpoint. - **Fix:** Check your plan tier or the key's scope. ## plan_feature_locked - **HTTP status:** 403 - **SDK class:** `PlanFeatureLockedError` (from `@hatched/sdk-js`) - **Meaning:** The customer's plan does not include the requested feature (e.g. Free tier hitting `/marketplace/*`). `err.requiredPlan` / `err.upgradeUrl` are on the error. - **Fix:** Do NOT retry. Branch on `err.code` and prompt an upgrade to `details.required_plan`. - **More:** [Guide →](/docs/billing/handling-402) ## publishable_key_scope - **HTTP status:** 403 - **SDK class:** `PublishableKeyScopeError` (from `@hatched/sdk-js`) - **Meaning:** Publishable key cannot mutate — use a secret key server-side. - **Fix:** Move the call to a server route. See Auth model. - **More:** [Guide →](/docs/concepts/auth-model) ## widget_token_scope > **Reserved — not currently emitted.** The API never returns this code today; it exists in the SDK for forward-compatibility. Listed here so the catalogue stays in sync with the SDK `ErrorCode` enum. - **HTTP status:** 403 - **SDK class:** `WidgetTokenScopeError` (from `@hatched/sdk-js`) - **Meaning:** Reserved — not currently emitted by the API. The SDK defines `WidgetTokenScopeError` defensively for a future widget-token scope check. - **Fix:** No action needed today; handle it the same as `forbidden` if you want forward-compatibility. ## not_found - **HTTP status:** 404 - **SDK class:** `NotFoundError` (from `@hatched/sdk-js`) - **Meaning:** Generic 404 fallback — a Nest route or guard threw a `NotFoundException` without the richer `resource_not_found` code (e.g. an unmatched route). - **Fix:** Verify the URL and the id; both map to `NotFoundError` in the SDK. ## resource_not_found - **HTTP status:** 404 - **SDK class:** `NotFoundError` (from `@hatched/sdk-js`) - **Meaning:** The referenced id does not exist or was archived. - **Fix:** Verify the id from a recent list/create response. ## active_egg_limit - **HTTP status:** 409 - **SDK class:** `ActiveEggLimitError` (from `@hatched/sdk-js`) - **Meaning:** The per-user active-egg cap was reached. `err.details.active` lists the existing eggs (id + status). - **Fix:** Hatch or cancel one of the listed eggs, or retry the create with `?ensure=true` (`eggs.create({ ..., ensure: true })`) to reuse one. - **More:** [Guide →](/docs/guides/first-user-bootstrap#common-pitfalls) ## config_version_mismatch - **HTTP status:** 409 - **SDK class:** `ConfigVersionMismatchError` (from `@hatched/sdk-js`) - **Meaning:** The buddy is pinned to a different config version than expected. - **Fix:** Migrate the buddy or pin your write to its current config. ## conflict - **HTTP status:** 409 - **SDK class:** `ConflictError` (from `@hatched/sdk-js`) - **Meaning:** A competing mutation won; retry is safe if you re-read state first. - **Fix:** Re-fetch the resource and retry the mutation. ## idempotency_key_conflict - **HTTP status:** 409 - **SDK class:** `ConflictError` (from `@hatched/sdk-js`) - **Meaning:** An earlier request reused this `Idempotency-Key` with a different body. The SDK surfaces this as a generic `ConflictError` with `code: "idempotency_key_conflict"`. - **Fix:** Use a fresh `Idempotency-Key` for the new request, or replay the exact same body to receive the cached response. - **More:** [Guide →](/docs/concepts/idempotency) ## no_published_config - **HTTP status:** 409 - **SDK class:** `NoPublishedConfigError` (from `@hatched/sdk-js`) - **Meaning:** The customer has no published config version yet, so eggs cannot be created. - **Fix:** Publish the gamification plan first. `err.details.publish_url` points at the dashboard publish page. - **More:** [Guide →](/docs/guides/first-user-bootstrap) ## validation_failed - **HTTP status:** 422 - **SDK class:** `ValidationError` (from `@hatched/sdk-js`) - **Meaning:** Request input failed validation (HTTP 422). Emitted for every validation path: class-validator DTO failures (via the global `ValidationPipe`), controllers that `Schema.parse(body)` with Zod, and domain business-rule checks. `err.details.fields` is a consistent `{ "": ["", ...] }` map across all three; Zod failures additionally include `err.details.issues` (the raw issue array). - **Fix:** Map `err.details.fields` onto your form/inputs, fix the offending values, and resend. - **More:** [Guide →](/docs/guides/troubleshooting#422-validation-failed) ## rate_limited - **HTTP status:** 429 - **SDK class:** `RateLimitError` (from `@hatched/sdk-js`) - **Meaning:** Over the per-minute quota. Honour `err.retryAfter` (seconds). - **Fix:** Let the SDK retry (default on) or backoff manually. - **More:** [Guide →](/docs/guides/troubleshooting#429-too-many-requests) ## internal_server_error - **HTTP status:** 500 - **SDK class:** `HatchedError` (base) - **Meaning:** An unhandled error reached the global filter. The `message` is the thrown error message; no stable `details` shape. - **Fix:** Not retryable as-is — capture `err.requestId` and report it. ## bad_gateway - **HTTP status:** 502 - **SDK class:** `HatchedError` (base) - **Meaning:** Generic 502 fallback when an upstream dependency failed without the more specific `upstream_image_error` code. - **Fix:** Transient upstream failure — retry with backoff. ## upstream_image_error - **HTTP status:** 502 - **SDK class:** `UpstreamImageError` (from `@hatched/sdk-js`) - **Meaning:** The art provider failed during hatch/evolve. No ledger writes happened. - **Fix:** Re-call the operation; idempotent. - **More:** [Guide →](/docs/guides/troubleshooting#502-upstream-image-error) ## onboarding_extract_failed - **HTTP status:** 503 - **SDK class:** `HatchedError` (base) - **Meaning:** The onboarding pipeline could not verify the gamification selections extracted from the generated plan. No state was persisted. `err.details.reason` is present when a reason was captured. - **Fix:** Retry plan generation after a short backoff. ## service_unavailable - **HTTP status:** 503 - **SDK class:** `HatchedError` (base) - **Meaning:** A dependency is temporarily unavailable (generic 503 fallback). - **Fix:** Transient — retry with backoff. ## Programmatic handling ```ts import { HatchedError, ValidationError, RateLimitError, InsufficientBalanceError, CreditInsufficientError, EventQuotaExceededError, PlanFeatureLockedError, } from '@hatched/sdk-js'; try { await hatched.buddies.spend(buddyId, { amount: 100, reason: "item" }); } catch (err) { if (err instanceof InsufficientBalanceError) { return showInsufficientFunds(err.balance, err.required); } if (err instanceof ValidationError) { return showFieldErrors(err.details); } if (err instanceof RateLimitError) { return retryLater(err.retryAfter); } // 402 billing family: never retry — branch on the code and show upgrade / top-up. if (err instanceof CreditInsufficientError) { return redirectToBilling(err.topUpUrl ?? err.upgradeUrl); } if (err instanceof EventQuotaExceededError) { return showQuotaWall(err.used, err.limit, err.resetAt, err.upgradeUrl); } if (err instanceof PlanFeatureLockedError) { return showUpgradePrompt(err.requiredPlan, err.upgradeUrl); } if (err instanceof HatchedError) { console.error(err.code, err.requestId, err.message); } throw err; } ``` --- # Rate limits > Per-principal plan-tier quotas, the 429 response shape, and how to back off gracefully. Source: https://docs.hatched.live/docs/reference/rate-limits Rate limits protect shared infrastructure. They're generous for normal product integrations but exist to prevent runaway loops. ## Quotas Rate limiting is **per principal** — one bucket per API key (hashed) — not per endpoint. Every request, regardless of route, draws from the same two sliding windows: a **per-minute** limit and a **per-day** limit. There are no per-second or per-endpoint quotas. `/events`, `/eggs` and `/widget-sessions` all consume the same bucket. The size of that bucket comes from the customer's **plan tier**: | Plan | Per minute | Per day | | --- | --- | --- | | Starter | 100 | 5,000 | | Growth | 1,000 | 50,000 | | Pro | 5,000 | 250,000 | | Enterprise | 50,000 | unlimited | A few routes use different buckets: | Bucket | Per minute | Per day | | --- | --- | --- | | Widget & embed-token routes (`/widget*`, `/embed-tokens`) | 3× the plan limit | 3× the plan limit | | Scoped principals (publishable key, widget/dashboard JWT) | 200 | 10,000 | | Public share & profile pages | 60 | 2,000 | | Unauthenticated routes | 20 | 200 | | Auth burst (login / register / password) | 10 | 100 | The windows are 60 seconds (per minute) and 86,400 seconds (per day). Higher plan tiers raise both the per-minute and per-day limits — talk to sales if you need a larger bucket. ## The 429 response ```http HTTP/1.1 429 Too Many Requests Retry-After: 60 Content-Type: application/json { "error": { "code": "rate_limited", "message": "Rate limit exceeded. Please retry after 60 seconds.", "requestId": "…" } } ``` The body has no `details` object. The wait value comes from the `Retry-After` response header — the full window in seconds (`60` for the per-minute limit, `86400` for the per-day limit), not a small value. The SDK reads that header into `RateLimitError.retryAfter` (defaulting to 60s if absent). ## Built-in retry The SDK retries 429s automatically (honouring `Retry-After`) up to `maxRetries` (default 3) with exponential backoff + jitter. You only need manual backoff for sustained overages or custom queue drains. ```ts new HatchedClient({ apiKey: process.env.HATCHED_API_KEY!, maxRetries: 5, // default 3 }); ``` ## Backoff pattern ```ts import { RateLimitError } from '@hatched/sdk-js'; async function sendWithBackoff(event) { for (let attempt = 0; attempt < 5; attempt++) { try { return await hatched.events.send(event); } catch (err) { if (err instanceof RateLimitError) { await sleep((err.retryAfter + Math.random()) * 1000); continue; } throw err; } } throw new Error('rate limit exhausted'); } ``` Add jitter (the `Math.random()` term) so concurrent callers don't all retry on the same millisecond. ## Bulk ingestion For high-volume backfills, don't serialise through `events.send`. Use `POST /events/batch` (SDK: `events.sendBatch`) instead: 1. Group up to **100 events per request** (the batch cap). 2. Each call processes **synchronously** and returns per-event results inline (see [HTTP API](/docs/reference/http-api)). 3. Accepted events are deduped by `(customer_id, event_id)`; each non-duplicate event counts against your monthly event quota. The batch endpoint shares the **same per-minute / per-day rate-limit bucket** as every other write — there is no cap bypass and no separate async throughput quota. A `402` (`event_quota_exceeded`) is returned if the monthly event quota is exhausted. ## Headers Every response includes: ``` X-RateLimit-Limit: 100 X-RateLimit-Remaining: 98 X-RateLimit-Reset: 1745327400 ``` `X-RateLimit-Limit` is your bucket's per-minute limit (here, `100` on the Starter plan). `X-RateLimit-Reset` is a Unix epoch-seconds timestamp marking the end of the current window. Use them to pace yourself *before* a 429 fires. --- # Plan capabilities > Which Hatched widgets, capabilities, and quotas land on which subscription tier. Auto-generated from packages/shared/src/pricing.ts. Source: https://docs.hatched.live/docs/reference/plan-capabilities import { Callout } from 'fumadocs-ui/components/callout'; This page is auto-generated from `packages/shared/src/pricing.ts`. The same constant drives the backend `PlanGuard`, the pricing page, and dashboard upgrade banners — so every gate you see in product is encoded below. Plan keys (`starter`, `growth`, `pro`, `enterprise`) map to public names Design Sandbox, Launch, Optimize, Govern. Backend guards always use the keys. ## Widget × Plan Which `data-hatched-mount="…"` values render successfully on each plan. A gated widget mount still loads but the API will return `403 plan_feature_locked` (with `details.required_plan` and `details.upgrade_url`) until the customer upgrades. If the plan covers the feature but an operator has turned it off in Settings, the API instead returns `403 capability_disabled`. | Widget | Design Sandbox (free) | Launch ($149/mo) | Optimize ($499/mo) | Govern (custom) | Capability flag | | --- | --- | --- | --- | --- | --- | | `buddy` | ✓ | ✓ | ✓ | ✓ | _always available_ | | `marketplace` | — | ✓ | ✓ | ✓ | `marketplace` | | `leaderboard` | ✓ | ✓ | ✓ | ✓ | _always available_ | | `badges` | ✓ | ✓ | ✓ | ✓ | `badges` | | `streak` | ✓ | ✓ | ✓ | ✓ | `streaks` | | `path` | — | — | ✓ | ✓ | `paths` | | `tokens` | ✓ | ✓ | ✓ | ✓ | `tokens` | | `kudos` | ✓ | ✓ | ✓ | ✓ | `kudos` | | `group-quest` | — | ✓ | ✓ | ✓ | `group_quest` | | `feed` | ✓ | ✓ | ✓ | ✓ | `seesaw_bump` | | `mystery-box` | — | ✓ | ✓ | ✓ | `mystery_box` | | `league` | — | ✓ | ✓ | ✓ | `leagues` | | `council` | — | — | — | ✓ | `council` | | `hexad-survey` | ✓ | ✓ | ✓ | ✓ | _always available_ | A widget marked _always available_ has no capability flag — every tier can mount it. `leaderboard` and `feed`-style surfaces fall here because they read data the platform always produces. ## Capability × Plan Raw `PlanFeatures` matrix. Backend `@RequiresCapability('foo')` decorators gate on these keys; the dashboard reads the same map to render locked-state banners. New capabilities must be added to `PlanFeatures` in `pricing.ts` before they can be referenced anywhere. | Capability | Design Sandbox (free) | Launch ($149/mo) | Optimize ($499/mo) | Govern (custom) | | --- | --- | --- | --- | --- | | `advanced_analytics` | — | — | ✓ | ✓ | | `badges` | ✓ | ✓ | ✓ | ✓ | | `boosters` | — | ✓ | ✓ | ✓ | | `cause_counter` | ✓ | ✓ | ✓ | ✓ | | `council` | — | — | — | ✓ | | `evolution` | — | ✓ | ✓ | ✓ | | `evolution_generative` | — | ✓ | ✓ | ✓ | | `founding_cohort` | — | — | ✓ | ✓ | | `gamification_planner` | ✓ | ✓ | ✓ | ✓ | | `generative_media` | — | ✓ | ✓ | ✓ | | `group_quest` | — | ✓ | ✓ | ✓ | | `hosted_surface` | — | — | ✓ | ✓ | | `kudos` | ✓ | ✓ | ✓ | ✓ | | `leagues` | — | ✓ | ✓ | ✓ | | `lottery` | — | ✓ | ✓ | ✓ | | `marketplace` | — | ✓ | ✓ | ✓ | | `mentorship` | — | — | ✓ | ✓ | | `multi_audience` | — | — | ✓ | ✓ | | `mystery_box` | — | ✓ | ✓ | ✓ | | `paths` | — | — | ✓ | ✓ | | `profile_pages` | — | — | ✓ | ✓ | | `profile_pages_v2` | — | ✓ | ✓ | ✓ | | `seesaw_bump` | ✓ | ✓ | ✓ | ✓ | | `showroom` | — | — | ✓ | ✓ | | `streaks` | ✓ | ✓ | ✓ | ✓ | | `teams` | ✓ | ✓ | ✓ | ✓ | | `tokens` | ✓ | ✓ | ✓ | ✓ | | `wardrobe` | ✓ | ✓ | ✓ | ✓ | ## Quotas + support | Plan | Events / month | Audiences | Welcome credits | Monthly credit grant | Support | | --- | --- | --- | --- | --- | --- | | Design Sandbox (free) | 10,000 | 1 | 20 | 0 | community | | Launch ($149/mo) | 500,000 | 1 | 0 | 50 | standard | | Optimize ($499/mo) | 5,000,000 | 3 | 0 | 250 | priority | | Govern (custom) | Unlimited | Unlimited | 0 | Unlimited | sla | Event quota counts inbound `POST /events` calls. Once a tenant hits the monthly cap the API returns `402 event_quota_exceeded` carrying the reset window (`details.reset_at`, the first of the next UTC month) and an upgrade link. It is **not** a `429` — do not retry with backoff; back off until the reset or upgrade the plan. SDK consumers catch this as `EventQuotaExceededError` — see [Error handling](/docs/guides/best-practices#error-handling). ## When you change pricing 1. Edit `packages/shared/src/pricing.ts` — `PLAN_MATRIX`. 2. Run `pnpm --filter @hatched/docs generate:plan-matrix` to regenerate this page. 3. Commit both files in the same PR. `generate-plan-matrix.ts --check` is part of CI so drift fails the build. --- # Health & version probes > Bare-path liveness, readiness, and build-fingerprint endpoints for Kubernetes, Fly.io, and uptime monitoring. Source: https://docs.hatched.live/docs/reference/health-version Hatched exposes three top-level probe paths intended for ops tooling. They serve the same purpose as the prefixed `/api/v1/health/*` endpoints — but they are separate handlers, not the same backend, and they skip the API prefix so K8s/Fly probes work out of the box without remapping paths. The human-readable multi-component report lives at `GET /api/v1/health`. | Endpoint | Purpose | Status code | | --- | --- | --- | | `GET /healthz` | Liveness — "the process is up" | always 200 | | `GET /readyz` | Readiness — DB + Redis reachable | 200 ready, 503 not ready | | `GET /version` | Build fingerprint | always 200 | The detailed multi-component breakdown (queues, image provider, latency per dependency) still lives at `GET /api/v1/health` for dashboards or on-call debugging. ## `GET /healthz` ```bash curl https://api.hatched.live/healthz # {"ok":true,"uptime":4623} ``` Returns immediately. Uptime is in seconds since process start. Use this as your Kubernetes `livenessProbe` — restart the container only if the process is genuinely wedged. ## `GET /readyz` ```bash curl -i https://api.hatched.live/readyz # HTTP/1.1 200 OK # {"ready":true,"db":true,"redis":true} ``` Returns `503` with `{"ready": false, ...}` if Postgres or Redis is unreachable. Use as the Kubernetes `readinessProbe` so the pod is removed from the load balancer until both deps are green. ## `GET /version` ```bash curl https://api.hatched.live/version # { # "version": "1.42.0", # "git_sha": "a1b2c3d4", # "built_at": "2026-05-25T09:14:30Z", # "node_version": "v20.11.0" # } ``` Useful as a deployment fingerprint: - Diff `git_sha` between expected and actual to confirm a rollout succeeded across all instances. - Compare the SDK consumer's `client.auth.whoami()` plan against the API's `version` when reproducing a bug — "I'm on 1.42.0 with plan growth" is a lot easier to triage than "I think it broke yesterday." The three env vars `API_VERSION`, `GIT_SHA`, and `BUILT_AT` are injected at build time by the deployment pipeline. Either may be `"unknown"` on local dev or in test environments where the build step is skipped. ## Why two paths per probe `/api/v1/health/live` (prefixed) and `/healthz` (bare) are separate handlers serving the same purpose — not the same handler. SDK calls and human dashboards use the prefixed form; ops tooling uses the bare form. Removing one of them would break either the SDK contract or the standard probe playbook — both are kept stable. Their response shapes and readiness semantics also differ, so do not treat them as interchangeable: - Liveness: bare `/healthz` returns `{"ok":true,"uptime":...}`, while prefixed `/api/v1/health/live` returns `{"alive":true,"uptime":...}`. - Readiness: bare `/readyz` checks only DB + Redis, while prefixed `/api/v1/health/ready` additionally checks the queue and image provider (returning `{"ready":...,"checks":{...}}`). The prefixed endpoint can therefore report `503` while bare `/readyz` still reports `200`. --- # Changelog > Release notes for @hatched/sdk-js — mirrored from the package's CHANGELOG.md. Source: https://docs.hatched.live/docs/reference/changelog {/* AUTO-MIRRORED from packages/sdk-js/CHANGELOG.md by apps/docs/scripts/generate-changelog.ts. */} Release notes for `@hatched/sdk-js`. Produced by [changesets](https://github.com/changesets/changesets) on every merge to `main`. # @hatched/sdk-js ## 1.2.0 ### Minor Changes - 48e9fdd: eggs.create now accepts a first-class audience option that binds the egg (and the buddy it hatches into) to a named audience. Audience-scoped content — streaks, badges, marketplace items — is keyed by audience, so a buddy born in the wrong audience makes those widgets 404 or render empty. The option is shorthand for metadata.audience and is folded in before the request, so the intuitive create(\{ userId, audience }) call sends the shape the API expects instead of being rejected by strict body validation. Single-audience workspaces can omit it; multi-audience workspaces should set it during the first-run bootstrap. ## 1.1.3 ### Patch Changes - fd3aff9: Refresh the generated OpenAPI types to cover two new dashboard endpoints: hosted-surface logo upload (`POST /customers/me/hosted-surfaces/{id}/logo`) and the admin event-trigger (`POST /events/admin-trigger`). This is a type-only refresh — no SDK resource methods or runtime behavior changed. ## 1.1.2 ### Patch Changes - e67d739: Analytics endpoints (engagement, activity-summary, economy-summary, economy-health, popular-items, popular-badges, retention, roi-metrics) now accept an optional `audience` query parameter, so metrics can be scoped to a single audience instead of the whole workspace. Omitting it returns the workspace-wide numbers as before. - e67d739: Generated API types now reflect the documented error contract for embed/session token creation and config-version publishing: a 404 is returned when the supplied buddy does not exist for the tenant, publishing an empty draft returns a 400, and the "already published" publish conflict is described more precisely (409 for a non-draft version). The new `GET /billing/checkout/session/{id}` checkout-reconcile endpoint is also now described, so a delayed or dropped Stripe webhook can be confirmed synchronously on dashboard return. A new `GET /customers/me/hosted-surfaces/{id}/players/{playerId}/access-code` endpoint lets a tenant admin re-view an existing hosted-surface player's access code and QR token without rotating them. These are additive response/endpoint-type refinements — existing request and success types are unchanged. ## 1.1.1 ### Patch Changes - a54b74c: Regenerated API types now cover the dashboard password endpoints (`POST /auth/password/change`, `POST /auth/password/reset/request`, `POST /auth/password/reset`) and the updated Player Zero response shape, with typed response bodies for each. ## 1.1.0 ### Minor Changes - 69b1331: Define cursor pagination as the canonical list shape (`{ data, pagination: { nextCursor, hasMore, limit } }`) and add `CursorPagination`, `CursorPaginationSchema`, and `CursorQuerySchema` types. The SDK's new `paginateCursor()`/`collectCursor()` helpers walk the envelope. Server-side, `apps/api/src/common/pagination/cursor.ts` provides a TypeORM helper that applies keyset filters and emits the canonical envelope — adopt it on new list endpoints. Existing offset-paginated endpoints are unchanged; consumers can mix both pagination styles. - 69b1331: Add `client.auth.whoami()` for validating an API key without performing a side-effectful call. Returns the customer id + name, plan, capability list entitled by that plan, the authenticating credential's id/label/type/scopes, and the auth method (`api_key` vs `dashboard_jwt`). Useful for onboarding CI checks and `--health` style scripts where the integrator wants to prove "my key is wired and has the scopes I think it does" before shipping. - 0984fa4: Add the `brag` resource for the F2.7 Brag Button — `client.brag.recordTelemetry()` records a share funnel step and `client.brag.sendSlackPost()` dispatches a Win-State brag to a tenant Slack/Teams webhook. Both map to explicit per-event user actions in the widget consent modal; there is no auto-share path. - 4d7c563: Add the Symbolic Cause Counter admin resource (F2.12). The tenant-admin `causes` resource backs the Planner "Humanity Hero — Cause Counter" drawer: list/create/update/delete cause definitions and run `preview30Days` to project symbolic units from the last 30 days of eligible events. New exported types `AdminCause`, `CreateCauseParams`, `UpdateCauseParams`, `CausePreview` and `CauseTriggerEvent`. - dbe33a0: Add the `eventBadges` resource for managing Event-Triggered Badge campaigns. A campaign binds a badge to a time window so any buddy active inside the window earns it once — the surprise, story-bound "you were here that day" badge. Supports list (with grant counts), create, update and delete from the tenant admin surface. - 0984fa4: Add a `feed` resource for the SeeSaw Bump team feed (F2.11). `client.feed.teamEvents.list({ cursor, limit })` returns the buddy's cursor-paginated team feed of auto-generated events, and `client.feed.teamEvents.clap(id)` toggles the idempotent 👏 clap reaction (a repeat call unclaps). New exported types `FeedResource`, `TeamEventsResource`, `TeamEvent`, `TeamEventKind`, `TeamEventsPage`, `ListTeamEventsParams`, and `ClapResult`. - 38a4d0e: Add the FlashSalesResource for managing marketplace flash sales (F3.9 Marketplace FOMO). Tenants can list, schedule and cancel flash sales via client.flashSales — a scheduled sale applies a temporary discount to a set of marketplace items for a bounded window, run by a once-a-minute API cron. - 4d7c563: Add the Founding Cohort admin resource (F2.13). The tenant-admin `foundingCohort` resource backs the Planner "Founding Cohort" drawer: `preview` projects how many buddies the current eligibility config would mark, `backfill` runs the one-shot retroactive assignment, and `listAudit` reads the assignment history. New exported types `FoundingCohortMode`, `FoundingCohortPreview`, `FoundingCohortBackfillResult` and `FoundingCohortAuditEntry`. - 0984fa4: Add Group Quest resources (F2.5). The widget-token `groupQuests` resource lets an embedded buddy list active quests (`groupQuests.list()`), join one (`groupQuests.join(id)`) and leave one (`groupQuests.leave(id)`); join is idempotent and returns `already_joined`. The tenant-admin `adminGroupQuests` resource backs the Planner drawer with CRUD, the `publish` transition and a manual `forceResolve` watchdog override. New exported types `ActiveGroupQuest`, `JoinGroupQuestResult`, `LeaveGroupQuestResult`, `AdminGroupQuest`, `GroupQuestStatus`, `GroupQuestRewardConfig`, `CreateGroupQuestParams`, `UpdateGroupQuestParams` and `ForceResolveResult`. - 9933e8f: Hexad survey resource + widget-token auth mode. **New `widgetToken` auth mode.** `HatchedClient` now accepts a third config shape — `{ widgetToken }` — alongside the existing `apiKey` (server) and `publishableKey` (browser) modes. Widget embed/session tokens are buddy-scoped and browser-safe; the client auto-allows them on `/widget/*` runtime paths and refuses everything else. - New export: `WidgetTokenConfig` config type. - New export: `WidgetTokenScopeError` — thrown without a network round-trip when a widget-token client calls a non-widget endpoint or a mutation that hasn't opted in via `allowWidgetToken`. - The runtime guard recognises widget tokens as browser-safe, so the browser-emit warning only fires for secret keys. **New `hexadSurvey` resource** for the Marczewski Hexad self-assessment widget. All four methods are widget-token scoped — admin recompute and aggregate endpoints stay dashboard-only. - `hexadSurvey.questions()` — fetch the 24-question axis catalog plus the current consent version. - `hexadSurvey.submit({ answers, consentVersion })` — UPSERT the widget user's Likert responses; returns the normalised six-axis distribution, primary type, and retention expiry. `consentVersion` is required; submitting without acknowledging the active version is rejected server-side. - `hexadSurvey.me()` — read back the user's stored response (or `null` when none). - `hexadSurvey.deleteMine()` — withdraw consent and purge the raw answers. - bc02f0d: Add the `kudos` resource for F2.3 peer recognition: `client.kudos.send()` posts a buddy→buddy kudos, `client.kudos.received()` and `client.kudos.given()` read the recent kudos feed and lifetime assist count. - bc02f0d: Add team scope to the leaderboard resource. leaderboard.get(\{ scope: 'team' }) now restricts the board to the requesting buddy's active team roster (HTCH-48), alongside the existing global scope. scope: 'friends' throws a client-side not-implemented error until Phase 3 ships. The response gains an optional effectiveTeamId field, and the scope-policy pill label is exposed as teamLabel. - 8e95453: Add the LeaguesResource for the F4.1 LEAGUES endgame feature. `client.leagues.me()` reads the widget buddy's live league standing — current tier, the next-tier promotion target, cohort standings, season countdown and the demotion-zone flag. The snapshot self-gates: an `available: false` result (plan not entitled, no active season, or buddy not enrolled) is a normal outcome callers render nothing for, never an error. - dbe33a0: Add the LotteryResource for managing recurring lotteries (F3.11 Rolling Reward). Tenants can list and CRUD lottery definitions via client.lottery, read past-draw history, fetch the live "next draw" preview and run a non-persisted draw simulation. A lottery silently enters any buddy that meets its eligibility rule into a weekly or monthly draw, resolved by a once-a-minute API cron. - bbc4600: Expose widget marketplace and outfit composition helpers, aura tier fields, and the new marketplace/aura webhook event types. - 0984fa4: Add `client.marketplace.gift()` for F2.4 Social Treasure gifting: a buddy can gift a marketplace item to a teammate, the sender pays and the item lands in the recipient's inventory. Marketplace items now expose `isGiftOnly` for items that can only be received as a gift. - 0984fa4: Add a `mentor` resource for widget consumers (F2.6 Mentorship, visibility-only). `client.mentor.setAvailability({ available })` toggles the buddy's mentor availability, `client.mentor.teamMentors(teamId)` lists a team's available mentors with server-rendered contact deep links, and `client.mentor.logSession({ hours, menteeLabel, summary })` / `client.mentor.sessionsForMe()` cover the self-reported Mentor Hours log. New exported types `MentorDirectoryEntry`, `MentorSession`, `MentorSessionsResponse`, `LogSessionParams`, and `LogSessionResult`. - 6618ae8: Add `nextBestAction` resource that returns the single highest-priority next-best-action for the widget session's buddy. The server orchestrates eight strategies (streak warning, almost-badge, dress-first-outfit, etc.), applies tenant overrides, and caches per buddy for 30 seconds — clients receive `{ action, fallbackUsed }`. Buddy responses now also expose `isNaked` to signal the Build-From-Scratch onboarding flow. - 06d98b6: Add `client.buddies.prestige()` and `client.buddies.prestigeStatus()` for the F4.3 Prestige Loop. A buddy that has reached the maximum evolution stage can prestige — reset to stage 0 in exchange for an incremented prestige level and a permanent prestige aura. `prestigeStatus()` reports whether the buddy can prestige and, when it cannot, the blocking reason (not at max stage, cooldown active, champion required, or the loop disabled for the workspace); `prestige()` performs the reset and returns the new prestige level, aura tint and reverted image. - dbe33a0: Add the `profileTemplates` resource for the F3.14 Profile Page Editor v2. Tenant admins can now list the template gallery (built-in system templates plus their own), create, update and delete custom profile-page templates, and bulk-apply a template to all buddies or a single audience. - 69b1331: `allowBrowser: true` is now refused at runtime unless the SDK can confirm it's being executed inside a test runner. Recognized markers are `NODE_ENV=test`, `VITEST`/`JEST_WORKER_ID` env vars, or the presence of `globalThis.expect` + `globalThis.it`. Outside a test runner, attempting to bypass the secret-key-in-browser guard throws immediately. This closes the "I copy-pasted my unit test config into production" footgun without changing any test-time behavior. - 69b1331: Export `ErrorCode` — a typed enum-shaped object whose keys map every known Hatched API error code (e.g. `ErrorCode.EventQuotaExceeded === 'event_quota_exceeded'`) — and `KnownErrorCode`, the union of those string values. Compare `HatchedError#code` against `ErrorCode.*` instead of typing string literals. Unknown codes still arrive on `err.code` as plain strings; the enum covers what the current SDK release knows about. - a95c14e: Align typed error, webhook, and operation surfaces with the live API. - **WebhookEvent now covers every server-emitted event (86, up from 26).** Handlers can exhaustively switch on the full set — kudos, group quests, leagues and the league-season lifecycle, council, mystery box, lottery, showroom, cause-counter, mentor, and lapsed-user re-engagement events are all typed instead of falling through as plain strings. - **ErrorCode gained the remaining server codes** (`bad_request`, `capability_disabled`, `onboarding_cap_reached`, `not_found`, `bad_gateway`, `service_unavailable`, `onboarding_extract_failed`, `internal_server_error`) so `err.code` comparisons stay typed across the whole surface. - **OperationStatus gained `cancelled`, and `operations.wait()` treats it as terminal.** A cancelled async operation previously polled until the timeout threw; it now returns as soon as the status is reached. - **`InsufficientBalanceError` reads `balance` / `required` from `err.details`** to match the wire shape (the API moved them under `details`); `err.balance` and `err.required` are populated again. It is now also raised for the `402` marketplace-gift path (a payment short on coins), not just the `400` spend path, and `err.statusCode` reflects whichever the API returned. - **A bare HTTP 400 now parses to a `HatchedError` that keeps its 400 status and code** (e.g. `bad_request`). Request-body/DTO validation moved server-side to `422 validation_failed` (surfaced as `ValidationError`), so a 400 is no longer mis-reported as a 422. - 69b1331: Add pluggable `SdkLogger` interface and `logger` config option. Pass a Pino/Winston/Bunyan instance — or any object with optional `debug`/`info`/`warn`/`error` methods — to replace the SDK's default `console.warn` output. Warn-level entries (literal-secret detection, retry hints) emit regardless of `debug`; debug-level request/response traces still gate on `debug: true`. Also exports a `consoleLogger()` factory for the default behavior. - 69b1331: Ship auto-generated OpenAPI types alongside the hand-written resources. `import type { paths, components, operations } from '@hatched/sdk-js'` exposes every public endpoint's exact request/response shape as derived from `apps/api/openapi.public.json`. The contract is regenerated via `pnpm --filter @hatched/sdk-js generate:types` and frozen in CI by `check:types`, so the SDK's typings can never drift from the API specification. Use the generated `paths['/buddies/{id}']['get']['responses']['200']['content']['application/json']` style references when you need a shape that doesn't have a hand-written equivalent yet. - 69b1331: Add async-iterator pagination helpers. `paginate()` + `collect()` walk offset-paginated endpoints (`{ data, meta: { total, page, limit } }`), while `paginateCursor()` + `collectCursor()` walk cursor-paginated endpoints (`{ data, pagination: { nextCursor, hasMore, limit } }`) — Hatched's canonical pagination shape for new list endpoints. All four return an `AsyncIterableIterator` that stops automatically when the consumer breaks or the page chain ends, and accept `maxPages` + `AbortSignal` for runaway protection. Reduces boilerplate when iterating over `buddies.list()`, `operations.list()`, and any new cursor-based endpoint. - 69b1331: Expose retry metadata via `hatched.getLastRetryMetadata()`. Returns `{ attempts, reasons, totalDelayMs, totalElapsedMs }` for the most recent request, where `reasons` is a per-retry list of `'429' | '5xx' | '408' | 'network'`. Lets consumers feed observability dashboards with how often the SDK had to retry to land a call, and inflate timeouts on hot paths where retries are routine. The new `RetryMetadata` type is exported alongside the existing `RateLimitSnapshot`. - ce6ff37: Fix webhook signature verification and the `whoami` response types. - **Webhook verification now actually works.** `WebhooksResource.verifySignature` and every framework adapter (`verifyExpressRequest`, `verifyFastifyRequest`, `verifyHonoRequest`, `verifyNextAppRequest`, `verifyNextPagesRequest`) previously parsed a `t=…,v1=…` header that the API never sends — so no real delivery could be verified. They now match the wire contract: `X-Hatched-Signature: sha256=` plus a separate `X-Hatched-Timestamp` header. Adapters read both headers automatically; the raw `verifySignature` accepts the timestamp via `options.timestamp`. The legacy combined format is still accepted for safety. - **`whoami()` types match the wire.** `WhoamiResult` and `ApiKeySummary` were declared snake_case (`customer_id`, `key_type`, …) but the client camelCases every response, so those fields were `undefined` at runtime. They are now camelCase (`customerId`, `keyType`, `lastUsedAt`, …). - **`leaderboard.get` no longer offers a throwing `scope: 'friends'`** — the unimplemented option was removed from the public type instead of throwing at runtime. - **User-Agent reports the real version.** The `SDK_VERSION` constant is now injected from `package.json` at build time, fixing a drift where it reported an older version. - bc02f0d: Add a `teams` resource for widget consumers — `client.teams.me()` returns the buddy's team, role and members, and `client.teams.leave(teamId)` leaves a team. New exported types `Team`, `TeamMember`, `TeamRole`, and `MyTeamResponse`. - 69b1331: Add framework adapters for verifying inbound webhook deliveries: `verifyExpressRequest`, `verifyFastifyRequest`, `verifyHonoRequest`, `verifyNextAppRequest`, `verifyNextPagesRequest`. Each adapter pulls the raw body + signature header from its framework's request shape and delegates to `WebhooksResource.verifySignature`, returning a uniform `{ valid, event, reason }` result. Both the package root and `@hatched/sdk-js/webhooks` deep import expose the adapters. - 69b1331: `WebhooksResource.verifySignature` and every framework adapter (`verifyExpressRequest`, `verifyFastifyRequest`, `verifyHonoRequest`, `verifyNextAppRequest`, `verifyNextPagesRequest`) now accept either a single secret or an array of secrets. Pass `[currentSecret, previousSecret]` during a rotation window to accept payloads signed under either secret without a separate code path. Adds `client.webhooks.rotateSecret(endpointId)` to call `POST /webhook-configs/:id/rotate-secret` and return the new plaintext secret. - ce6ff37: Add five widget-token resources so embedded buddy widgets can drive their in-app surfaces with full types: `client.notifications` (feed list, unread count, mark read / dismiss / dismiss-all / snooze), `client.mysteryBox` (state + claim), `client.council` (list my narrative proposals + submit a proposal), `client.freeLunch` (pending welcome notification + acknowledge), and `client.beginnersLuck` (hatch-ceremony result). Each wraps the corresponding `/widget/*` endpoint with hand-defined response types matching the API JSON. A new coverage test ratchets the SDK against the public spec's `/widget/*` surface so future widget endpoints can't silently ship without either an SDK resource or an explicit acknowledgement. - cf3f560: Add the `players` resource and sync webhook + response contracts with the server. - New `players.zero()` (idempotent create-or-get of the workspace demo player, user id `player-0`) and `players.zeroStatus()` (read-only existence/hatch check) — previously this endpoint was only reachable by hand-rolled HTTP. - `WebhookEvent` union now matches the server catalog exactly: adds `buddy.ceremony_completed`, `free_lunch.granted`/`free_lunch.seen`/`free_lunch.dismissed`, `group_quest.joined`/`group_quest.left`, `outfit.saved`/`outfit.worn`/`outfit.deleted`, `gate.unlocked` and `skill.decayed`; removes `marketplace.next_drop_announced` and `self_awareness.profile_revealed`, which were documented but never emitted. - Delete acknowledgements are now typed against the standard `{ ok: true }` success envelope instead of per-endpoint `{ deleted: true }` shapes. ### Patch Changes - 0eb7ba0: Add typed widget `badges.list({ includeLocked })` and `leaderboard.get(...)` SDK resources for the badge collection grid and leaderboard view-mode APIs. - 06d98b6: Add `client.leagues.bossFightProgress()` for the F4.2 Boss Fight seasonal challenge. It returns the widget buddy's progress toward the season-long target — the name and description, the target metric and value, the buddy's own progress and completion rank, the deadline, and the challenge leaderboard. An unavailable view is a normal outcome (no active boss fight) and callers render nothing. - 8e95453: Add `client.leagues.seasonHighlights(seasonId)` for the F4.1a Season Closing Ceremony. It returns a buddy's four personalized season highlights (best week, kudos sent, items collected, cohort role), each z-scored against the cohort so callers can auto-pick the most brag-worthy moment, plus the season outcome, final rank and public-share code. - 06d98b6: Add `isMentor` to the `Buddy` type — true when a buddy holds the mentor role (F4.4 Mentor Role tier). Also fixes `buddies.prestigeStatus()` / `buddies.prestige()` to read the camelCased response correctly, so their fields are no longer undefined at runtime. - 1cf026d: Add `ErrorCode.MissingAudience` (`missing_audience`) and `ErrorCode.UnknownAudience` (`unknown_audience`) to the known error-code enum. The API already returns these 400s when a request to a multi-audience customer omits or mis-names the `audience` field; the SDK now recognises them in `KnownErrorCode` so callers can switch on them exhaustively. - 569de43: SDK README header gains a one-line tagline ("The motivation API your backend already speaks.") above the existing intro paragraph, and the intro picks up a single-sentence bridge to the Yu-kai Chou Octalysis framework — the data model the SDK was shaped around. The npm `description` field is revised to lead with the runtime + language + product + framework hook + security default in one line ("TypeScript-first Node SDK for Hatched — the B2B gamification platform with Octalysis-native scoring. Server-only; secret keys never in the browser."). No runtime change; package metadata + README markup only. - 0984fa4: Add a `socialNorms` resource for widget consumers (F2.9). `client.socialNorms.today()` returns the buddy's positive-framing team norms for today — completion, momentum, and elitism framings, server-rendered and believability-gated. New exported types `SocialNorm`, `SocialNormFraming`, and `SocialNormsTodayResponse`. ## 1.0.0 ### Major Changes - 6dff237: PathDisplayMode renamed: replace `'path'` with `'straight'` (clean centered column) and add `'zigzag'` (Duolingo-style alternating sides). Existing `'path'` consumers must migrate to `'straight'`. The legacy half-zigzag rendering (node offset, text static) is removed. ### Minor Changes - d73cafe: First-run bootstrap ergonomics: `eggs.create({ userId, ensure: true })` now reuses the user's existing waiting/ready egg instead of creating a new one (avoids the per-user active-egg cap on retries). `Egg` responses include `buddyId` (non-null once the egg is hatched). Two new typed 409 errors are surfaced: `NoPublishedConfigError` (raised by `eggs.create` before a config version is published — exposes `publishUrl`) and `ActiveEggLimitError` (raised when the active-egg cap is hit — exposes `max` and `active[]` with the existing egg ids/statuses). ## 0.5.0 ### Minor Changes - d758872: Add `paths` resource — guided multi-step journeys (Duolingo-style). New `client.paths` exposes admin CRUD over path definitions, steps, and sub-steps; `setActive()` flips the audience's single active path atomically; `getForBuddy()` returns the buddy-scoped runtime payload with locked / available / completed sub-step states; `completeSubStep()` manually marks a sub-step done and returns cascade flags so callers can celebrate without an extra round-trip. Sub-steps support an optional `contentUrl` + `ctaLabel` for deep-linking into a customer's LMS. `events.send()` response now also exposes `streakUpdates` and `pathUpdates` on the returned `EventEffects`, so a single track call can drive streak counters and path widgets in addition to coins/badges without an extra fetch. - 42907d4: Added new feature: path and performance improvements ## 0.4.4 ### Patch Changes - ea76743: Republish 0.4.3 with corrected package metadata: `repository.url` and `bugs.url` now point to the public SDK repo at `github.com/hatched-live/hatched-sdk-js`. (0.4.3 was never published to npm.) ## 0.4.3 ### Patch - **Buddy appearance types** — `Buddy` now exposes `baseImageUrl`, typed equipped item objects, and the `appearance` status block used by the persistent AI compositing pipeline. - **Equip result typing** — `buddies.equip()` now returns a typed `{ accepted, operationId, status, appearanceStatus, cached }` result so clients can distinguish instant cache hits from queued appearance generation. - **Rerender appearance** — new `buddies.rerenderAppearance(buddyId)` method backed by `POST /buddies/:id/appearance/rerender`. Use when `appearance.status === 'failed'` (especially `error.code === 'needs_rerender'` after the appearance pipeline migration) to regenerate the bare stage image. Equipped items are cleared from the rendered set; re-equip after status returns to `ready`. ### Behavior change (non-breaking type-wise, breaking semantically) - **Evolution no longer blocks on item composite failure.** Previously, if the bare stage image succeeded but compositing equipped items failed, the buddy stayed on the prior stage and the evolve operation was marked failed. Now the buddy advances to the new stage with its bare base image and `appearance` reports `failed` or `awaiting_credits`. The evolve operation completes successfully and the appearance recovery flow re-attempts the composite. - **Awaiting-credits self-recovery.** When equip/evolve hits a credit limit, an internal scheduler retries with exponential backoff (60s → 5m → 15m → 30m). Clients should poll `buddy.appearance` rather than the original operation status to track final resolution. ## 0.4.1 ### Patch - **Docs domain** — all references now point to `docs.hatched.live` (single canonical domain). The legacy `docs.hatched.com` host is retired; error messages, README links, and `package.json.homepage` have been updated. No API or behavior changes. ## 0.4.0 ### Minor - **Publishable-key hardening** — browser keys can mint scoped read-only embed tokens, but no longer mint interactive widget sessions. Interactive session tokens stay server-only. - **Webhooks resource alignment** — SDK methods now target the production `/webhook-configs` API and unwrap dashboard response envelopes. - **Docs/examples refresh** — README snippets, widget examples, and auth guidance now match the current CDN loader and `Authorization: Bearer` API model. ## 0.3.0 ### Major - **Two-tier token model**. Tokens now have an explicit `kind`: `primary` (spendable via `buddies.spend`, marketplace purchases, gate unlocks) or `progression` (earn-only, feeds evolution readiness). `token_config` DTOs unlocked from the legacy 4-tuple (`hatch_token`/`evolution_token`/`reroll_token`/`gift_token`) — customers now pick their own `token_key` (e.g. `gems`, `mana`, `xp`). Spending a progression token returns `progression_not_spendable`. - **Canonical item categories**. The two coexisting taxonomies (`hat`/`held_item`/… vs `headwear`/`eyewear`/…) collapse into 8 canonical slots: `background`, `body`, `feet`, `hand`, `neck`, `face`, `head`, `accessory`. Migration 023 normalises existing rows and locks the column via CHECK. ### Minor - **`buddies.tokens(buddyId)`** — typed primary + progression balance snapshot with lifetime earn/spend sums. - **`buddies.evolutions(buddyId)`** — paginated stage-transition timeline (prod + demo). Backed by a new `buddy_evolutions` table that captures every evolve with its image and trigger event. - **`GatesResource`** — new `hatched.gates.unlock(buddyId, gateKey)` / `unlocks(buddyId)` primitive. Customers author gates in the dashboard (`gate_key`, `token_key`, `cost`, `metadata`); end-users spend tokens to unlock features. Unlock is idempotent — repeat calls return `alreadyUnlocked: true` without touching the economy. - **Equip safety rails** — `TooManyItemsError` (max 4 equipped) and `CategoryConflictError` (two non-accessory items in the same category) surface at the SDK layer with `details` carrying the specifics. - **Stage-aware item artwork** — `items.stage_image_urls` jsonb lets designers ship stage-2-specific hats; the composite pipeline picks the right variant per stage. - **Evolve×equip pipeline (pre-0.4.3)** — the initial implementation attempted to block `operations.wait(evolveOp)` until equipped items were composited. In 0.4.3 this was replaced by `buddy.appearance` status recovery so stage advancement can complete even when item compositing is delayed. - **Theme-aware defaults** — empty marketplace or token bundle at onboarding seeds from a theme-appropriate catalog (fantasy → gems/mana + fantasy items, fitness → reps/streaks, etc.). Source tracked in `customers.settings.applied_sources`. ## 0.2.1 ### Patch - **Docs**: README now has a dedicated "Two ways to authenticate" section with a secret-vs-publishable key comparison, a browser-safe publishable-key example, and a per-resource secret/publishable capability matrix. No code changes. ## 0.2.0 ### Major - **camelCase public surface** — all params and response fields exposed by the SDK are now camelCase. Snake_case wire format is converted transparently. Migration: rename `user_id` → `userId`, `event_id` → `eventId`, `occurred_at` → `occurredAt`, etc. The same applies to response fields (`egg.egg_id` → `egg.eggId`, `op.operation_id` → `op.operationId`). - **Operation.wait** — `operations.waitForCompletion` has been replaced with the shorter `operations.wait`. The old name is still exported as a deprecated alias. ### Minor - **Server-only runtime guard** — the SDK now throws when constructed in a browser-like environment with a secret key. Override with `allowBrowser: true` (test-only). - **Publishable key support** — browser-safe `HatchedClient({ publishableKey })` constructor variant. Only read endpoints and `embedTokens.create` are allowed; mutation methods return `PublishableKeyScopeError` at runtime. - **Automatic retries** — exponential backoff + jitter on network errors, 408, 429 (retry-after honoured), and 5xx. Configurable via `maxRetries` (default 3). - **AbortSignal on every method** — pass `signal` to cancel in-flight requests; combined with the internal timeout via `AbortSignal.any`. - **Request id tracking** — `hatched.getLastRequestId()` exposes the `X-Request-Id` of the most recent response. SDK-generated request ids are sent on every call. - **Webhooks resource** — `hatched.webhooks.list/create/delete/deliveries/replay` + `WebhooksResource.verifySignature(rawBody, header, secret)` for `Hatched-Signature` verification. - **New error classes** — `AuthError` (base for 401/403), `PublishableKeyScopeError`, `ConfigVersionMismatchError`, and a `ResourceNotFoundError` alias for `NotFoundError`. - **tsup dual build** — `dist/index.mjs`, `dist/index.cjs`, plus `.d.ts`/`.d.cts`. Subpath exports for tree-shaking: `@hatched/sdk-js/errors`, `@hatched/sdk-js/webhooks`. - **`sideEffects: false`** — enables aggressive tree-shaking by bundlers. - **`timeoutMs` alias** — equivalent to `timeout`, aligns with the docs. - **`fetch` override** — supply a custom `fetch` implementation for edge runtimes and tests. ### Fixes - Correct URL concatenation — paths now preserve the base `/api/v1` prefix (previously absolute paths could drop it). ## 0.1.1 Initial private-preview release. --- # Pricing > Fixed platform fee + metered AI credits. Four plans (Free, Growth, Pro, Enterprise). Source: https://docs.hatched.live/docs/billing/pricing Hatched separates **platform fee** from **AI usage**. - The platform fee covers everything that's free to run at scale: events, rules engine, widgets, analytics, SDK, webhooks, dashboard, config versions. - AI usage — image generation, onboarding chat, plan/theme/guide generation — is metered in **credits** at a flat rate: **1 credit = $0.10 = 1 completed AI job**. ## Plans | Plan | Monthly | Events / mo | Credits included / mo | Welcome credits | | ------------ | ------- | ----------- | --------------------- | --------------- | | Free | $0 | 10,000 | 0 | 20 (one-time) | | Growth | $149 | 500,000 | 50 | — | | Pro | $499 | 5,000,000 | 250 | — | | Enterprise | custom | contract | contract | — | Billing is monthly via Stripe. Annual billing is available on Growth/Pro at ~17% discount (`$1,490/yr` and `$4,990/yr`). ## Feature gates The capability matrix below is the runtime `PLAN_MATRIX` in [`packages/shared/src/pricing.ts`](https://github.com/Flalingo/hatched/blob/main/packages/shared/src/pricing.ts). That file is the single source of truth — if anything below disagrees with the API guard, the API wins. (See Codex review fix #28.) | Capability | Free | Growth | Pro | Enterprise | | --------------------------------- | ---- | ------ | ------- | ---------- | | Skills, coins, badges, streaks | ✓ | ✓ | ✓ | ✓ | | Tokens (secondary currency) | ✓ | ✓ | ✓ | ✓ | | Leaderboards, webhooks, SDK, keys | ✓ | ✓ | ✓ | ✓ | | Gamification Planner | ✓ | ✓ | ✓ | ✓ | | Wardrobe / Cause counter / Kudos | ✓ | ✓ | ✓ | ✓ | | Teams / SeeSaw Bump | ✓ | ✓ | ✓ | ✓ | | Marketplace | — | ✓ | ✓ | ✓ | | Evolution | — | ✓ | ✓ | ✓ | | Evolution — generative art | — | ✓ | ✓ | ✓ | | Generative media | — | ✓ | ✓ | ✓ | | Mystery Box / Boosters | — | ✓ | ✓ | ✓ | | Group Quest | — | ✓ | ✓ | ✓ | | Leagues / Lottery | — | ✓ | ✓ | ✓ | | Profile Pages v2 | — | ✓ | ✓ | ✓ | | Paths | — | — | ✓ | ✓ | | Multi-audience | — | — | up to 3 | contract | | Advanced analytics (cohorts) | — | — | ✓ | ✓ | | Mentorship | — | — | ✓ | ✓ | | Founding Cohort / Profile Pages | — | — | ✓ | ✓ | | Council (endgame elitism) | — | — | — | ✓ | When a plan-locked endpoint is called, the API returns `403 plan_feature_locked` with `details.required_plan` so the caller can prompt an upgrade. ## Widget availability by plan Hatched ships **14 mountable widgets**. The plan a widget needs is derived from the same `PLAN_MATRIX` capabilities above — this table just maps each widget's public mount name to its minimum plan. Each widget's data attributes, scopes and endpoints live in the [widget reference](/docs/reference/widgets). | Widget (`data-hatched-mount`) | Token | Free | Growth | Pro | Enterprise | | ----------------------------- | ------ | ---- | ------ | --- | ---------- | | `buddy` | embed | ✓ | ✓ | ✓ | ✓ | | `badges` | embed | ✓ | ✓ | ✓ | ✓ | | `streak` | embed | ✓ | ✓ | ✓ | ✓ | | `tokens` | embed | ✓ | ✓ | ✓ | ✓ | | `leaderboard` | embed | ✓ | ✓ | ✓ | ✓ | | `kudos` | session| ✓ | ✓ | ✓ | ✓ | | `feed` | embed | ✓ | ✓ | ✓ | ✓ | | `hexad-survey` | session| ✓ | ✓ | ✓ | ✓ | | `marketplace` | session| — | ✓ | ✓ | ✓ | | `group-quest` | session| — | ✓ | ✓ | ✓ | | `mystery-box` | session| — | ✓ | ✓ | ✓ | | `league` | embed | — | ✓ | ✓ | ✓ | | `path` | embed | — | — | ✓ | ✓ | | `council` | session| — | — | — | ✓ | The **Token** column is the token a *write*-capable widget needs: `session` widgets perform writes (purchase, send, join, open, submit) and must be mounted with a server-minted session token, while `embed` widgets are read-only. Where the table and `PLAN_MATRIX` ever disagree, `PLAN_MATRIX` wins. ## Event quota enforcement Each plan has a monthly event ingestion quota. Requests include `X-Event-Quota-*` response headers; crossing 80% triggers a dashboard banner and a `usage.threshold_reached` webhook; crossing 100% returns `402 event_quota_exceeded` until the monthly reset (first of the next UTC month) or a plan upgrade. ## Related - [Credits](/docs/billing/credits) - [Stripe portal](/docs/billing/stripe-portal) - [Handling 402](/docs/billing/handling-402) --- # Credits > How the credit pool works, which jobs cost credits, and the spend order. Source: https://docs.hatched.live/docs/billing/credits **1 credit = $0.10 = 1 completed AI job.** Every call to Hatched's generative pipeline authorizes one credit up-front, runs, then commits (success) or rolls back (failure). Preset and cache-hit paths cost zero credits. ## Job types | Job | Credits | | ---------------------------- | ------- | | Onboarding chat turn | 1 | | Website scan | 1 | | Plan generation / regeneration | 1 | | Theme synthesis | 1 | | Integration guide | 1 | | Hatch (creature) | 1 | | Equip / composite edit | 1 | | Evolve (generative) | 1 | | Badge icon | 1 | | Skill icon | 1 | | Marketplace item image | 1 | | Stage asset | 1 | | Preset / cache-hit evolution | 0 | | Automatic asset prompt preparation | 0 | ## Three pools Credits live in three pools, spent in this order: 1. **promo** — time-limited bonus credits from top-up bundles. They expire 365 days after purchase (surfaced as `promo_expires_at`) and are spent first. 2. **welcome** — 20 one-time credits granted on signup to the Free plan, never expires while the account exists. 3. **paid** — top-ups and subscription grants. Top-up credits persist while the account exists and the payment is not refunded. A single 1-credit job debits from exactly one pool — no splitting across pools. If the pool with enough balance for the requested cost exists, we use it; otherwise the API returns `402 credit_insufficient`. ## Monthly subscription grant Growth grants 50 credits each month on successful `invoice.payment_succeeded`. Pro grants 250. Granted credits go to the **paid** pool. Paid credits do not expire until the subscription ends. ## Top-up bundles Bought via Stripe Checkout from the Billing page. The smallest bundle is available on every plan including Free; the larger bundles require **Growth** or above. Base credits land in the **paid** pool and never expire; bonus credits land in the **promo** pool and expire 365 days after purchase. The effective per-credit price below accounts for the bonus. | Bundle | Base | Bonus | Total | Price | Effective per-credit | Plans | | ------ | ----- | ----- | ----- | ----- | -------------------- | ------- | | 100 | 100 | — | 100 | $10 | $0.100 | Free+ | | 500 | 500 | +50 | 550 | $50 | $0.091 | Growth+ | | 1,000 | 1,000 | +150 | 1,150 | $99 | $0.086 | Growth+ | | 2,500 | 2,500 | +500 | 3,000 | $249 | $0.083 | Growth+ | ## Reading the balance ```http GET /api/v1/credits/balance Authorization: Bearer hatch_live_… ``` ```jsonc { "welcome": 7, "paid": 43, "promo": 0, "promo_expires_at": null, "total_spendable": 50 } ``` Every authenticated response also includes: - `X-Credits-Remaining` - `X-Credits-Welcome-Remaining` - `X-Credits-Paid-Remaining` - `X-Credits-Promo-Remaining` ## Ledger `GET /api/v1/credits/ledger?limit=50` returns the 50 most recent AI usage rows (authorize/commit/rollback) — the same rows the dashboard's Billing page displays. ## Onboarding cap During the very first publish, Hatched generates at most **4 images** (creature, one stage preview, one badge, one item). Remaining badge/item icons stay in `pending` and surface as "Generate now" actions in the dashboard — the operator pays 1 credit per asset they actually need. --- # Stripe portal > Subscription management, invoices, and top-up purchases. Source: https://docs.hatched.live/docs/billing/stripe-portal All subscription and top-up actions route through the **Stripe Customer Portal**. Hatched does not ship its own billing UI — the portal is the single source of truth for payment methods, invoices, plan switches, and credit bundle purchases. ## Open the portal From the dashboard: **Billing → Manage billing** opens a portal session scoped to the signed-in customer. Under the hood: ```http POST /api/v1/billing/portal Authorization: Bearer Content-Type: application/json { "flow": "default" } ``` ```jsonc { "portal_url": "https://billing.stripe.com/p/session/…" } ``` `flow` can be: - `default` — the full portal (subscription, invoices, payment method, credit add-ons) - `top_up` — deep-link to the credit bundle add-on flow - `cancel` — deep-link to the cancel confirm ## Subscription checkout (for new customers) Free plan customers upgrading to Growth or Pro: ```http POST /api/v1/billing/checkout Content-Type: application/json { "flow": "subscription", "plan": "growth" } ``` ```jsonc { "checkout_url": "https://checkout.stripe.com/c/pay/cs_…" } ``` ## One-off credit bundle Top-ups use a one-off Stripe Checkout session (or the portal's add-on UI). ```http POST /api/v1/billing/checkout Content-Type: application/json { "flow": "credit_bundle", "credit_bundle": "100" } ``` Valid bundle keys: `"100"`, `"500"`, `"1000"`, `"2500"`. The `"100"` bundle is available on every plan; `"500"`/`"1000"`/`"2500"` require Growth or above. The `checkout.session.completed` webhook (mode=payment) grants credits into the **paid** pool atomically, keyed on `stripe_event_id` so a double delivery is a no-op. ## Webhook handling Hatched subscribes to the following Stripe events: | Event | Effect | | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- | | `checkout.session.completed` (subscription) | Set `customer.plan`, grant the plan's included credits — full 12-month allotment upfront for annual, one month's worth for monthly. | | `checkout.session.completed` (payment) | Top-up: split the bundle into `paid` pool (non-expiring) + `promo` pool (bonus credits, expire after `bonus_expires_days`). | | `invoice.payment_succeeded` (subscription_cycle) | Grant the plan's included credits for the cycle — 12 months upfront on annual renewal, one month on monthly renewal. | | `invoice.payment_failed` | Set `billing_status = past_due`. | | `customer.subscription.updated` | Reconcile plan and status from Stripe truth. | | `customer.subscription.deleted` | Downgrade to `starter`, keep paid credits. | | `charge.refunded` | Automatically claws back top-up credits (paid + bonus/promo) proportionally to the refunded amount, clamped to the live balance and idempotent per charge. Subscription-invoice refunds are logged with no clawback. | All credit grants are idempotent on `stripe_event_id` via `credit_transactions.uq_credit_tx_stripe_event`. ## Stripe product setup One-time setup in the Stripe dashboard: 1. Create two recurring products for plans: - `Hatched Growth Monthly` → `price_growth_monthly` → env `STRIPE_GROWTH_PRICE_ID` - `Hatched Growth Annual` → `price_growth_annual` → env `STRIPE_GROWTH_ANNUAL_PRICE_ID` - `Hatched Pro Monthly` → `price_pro_monthly` → env `STRIPE_PRO_PRICE_ID` - `Hatched Pro Annual` → `price_pro_annual` → env `STRIPE_PRO_ANNUAL_PRICE_ID` 2. Create four one-off products for top-ups: - 100 credits · $10 → env `STRIPE_CREDITS_100_PRICE_ID` - 500 credits + 50 bonus · $50 → env `STRIPE_CREDITS_500_PRICE_ID` - 1,000 credits + 150 bonus · $99 → env `STRIPE_CREDITS_1000_PRICE_ID` - 2,500 credits + 500 bonus · $249 → env `STRIPE_CREDITS_2500_PRICE_ID` 3. In **Customer Portal**, enable subscription update (Growth ↔ Pro), cancel, invoice history, and **customer-initiated one-off purchases** scoped to the four credit bundle products. Save the configuration id to `STRIPE_PORTAL_CONFIGURATION_ID`. --- # Handling 402 responses > credit_insufficient, event_quota_exceeded — what they mean and how to recover. Source: https://docs.hatched.live/docs/billing/handling-402 Hatched uses HTTP `402 Payment Required` for two conditions the caller can fix by topping up or upgrading: - `credit_insufficient` — no pool has enough credits for the requested AI job. - `event_quota_exceeded` — the monthly event quota for the plan is exhausted. Plus `403 plan_feature_locked` when a plan doesn't include the requested feature at all (e.g. Free plan hitting `/marketplace/*`). ## Envelope All three errors share the canonical envelope: ```jsonc { "error": { "code": "credit_insufficient", "message": "Not enough credits for this AI job (need 1, have 0).", "details": { "required": 1, "available": 0, "welcome": 0, "paid": 0, "promo": 0, "upgrade_url": "https://hatched.live/dashboard/billing", "top_up_url": "https://hatched.live/dashboard/billing?action=top_up" }, "requestId": "req_abc123" } } ``` ## Do NOT retry Neither 402 nor 403 are transient. **Do not wrap them in exponential backoff**. The SDK's built-in retry only kicks in for 429 and upstream 5xx; 402/403 are surfaced to the caller immediately. ## Recover - `credit_insufficient` → send the user to `details.upgrade_url` or `details.top_up_url` (both deep-link into the dashboard Billing page; the dashboard then opens the Stripe portal via `POST /billing/portal`). - `event_quota_exceeded` → back off until `details.reset_at` (first of next UTC month) or upgrade to a higher plan. - `plan_feature_locked` → prompt upgrade to `details.required_plan`. ## SDK recipe ```ts import { HatchedClient, CreditInsufficientError, EventQuotaExceededError, PlanFeatureLockedError, } from '@hatched/sdk-js'; const hatched = new HatchedClient({ apiKey: process.env.HATCHED_API_KEY! }); try { await hatched.events.send({ userId: 'u_1', type: 'lesson_completed' }); } catch (err) { if (err instanceof EventQuotaExceededError) { console.warn( `Event quota exceeded (${err.used}/${err.limit}). Resets ${err.resetAt}.`, ); redirect(err.upgradeUrl!); } else if (err instanceof CreditInsufficientError) { redirect(err.topUpUrl ?? err.upgradeUrl!); } else if (err instanceof PlanFeatureLockedError) { showUpgradePrompt(err.requiredPlan, err.upgradeUrl); } else { throw err; } } ``` ## Response headers Every authenticated response includes credit / quota metadata in headers so you can warn the operator before they hit the wall: - `X-Credits-Remaining`, `X-Credits-Welcome-Remaining`, `X-Credits-Paid-Remaining`, `X-Credits-Promo-Remaining` - `X-Event-Quota-Limit`, `X-Event-Quota-Used`, `X-Event-Quota-Remaining`, `X-Event-Quota-Reset-At` - `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` ## Webhook signal When a customer crosses 80% of their monthly event quota we emit one `usage.threshold_reached` webhook event (`limit_type: 'event_quota'`). The 100% boundary is not webhooked — it is hard enforced via 402 and surfaced in the dashboard banner.