Getting started
Ten minutes from zero to a buddy in your product — create an egg, send your first event, embed a widget.
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 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, thesnake_caseraw API, and hatch latency. Skipping those is the #1 cause of broken first-run integrations.
1. Sign up and grab an API key
- Create an account at the Hatched dashboard.
- Generate/apply a plan or pick a dashboard preset —
language-learning,fitness,productivity, orcustom. That step creates the event types the first event will use, such aslesson_completed, so the first ingest does not fail withevent_type_not_registered. - Publish your config. Picking a preset in step 2 publishes your first
config version automatically, so
eggs.createworks straight away. (If you built a config from scratch, open the rules editor and hit Publish —eggs.createreturns409 no_published_configuntil one is published. Later edits also sit on a draft until you publish them.) New buddies pin to the snapshot you publish. - Go to Developers → API keys and create a secret key (prefix
hatch_live_in production,hatch_test_for sandbox). - 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
pnpm add @hatched/sdk-js
# or
npm install @hatched/sdk-jsimport { 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.
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
has the full reuse pattern. ensure: true makes the create call reuse this
user's existing waiting/ready egg if there is one.
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.)# 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_idimport 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.
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);
}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'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_usermeans theuser_id/audience has no active buddy yet. Reuse thebuddyIdfrom step 3, or hatch one before testing events.no_matching_rulesmeans 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_registeredresponse 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.
The 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:
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:
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.
Pass the token to the client and render the widget:
<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 — react on your backend when a buddy earns a badge or hits a streak milestone.
- Configure rules — tune the coin economy and badge conditions.
- Reference — the full API spec.
- Auth model — secret vs publishable keys.
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.
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.