`.
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