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

<AuthBadge kind="api-key">
  Secret keys are server-only. Never ship one to a browser bundle.
</AuthBadge>

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

<Tabs items={['TypeScript', 'curl', 'Python']}>
  <Tab value="TypeScript">
    ```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.)
    ```
  </Tab>
  <Tab value="curl">
    ```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
    ```
  </Tab>
  <Tab value="Python">
    ```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}")
    ```
  </Tab>
</Tabs>

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.

<Tabs items={['TypeScript', 'curl', 'Python']}>
  <Tab value="TypeScript">
    ```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);
    }
    ```
  </Tab>
  <Tab value="curl">
    ```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'
    ```
  </Tab>
  <Tab value="Python">
    ```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)
    ```
  </Tab>
</Tabs>

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
<script
  src="https://cdn.hatched.live/widget.js"
  data-session-token="{{session.token}}"
  defer
></script>

<div data-hatched-mount="buddy"></div>
```

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.
