HatchedDocs
Guides

SDK quickstart

Install @hatched/sdk-js, authenticate, and make your first calls from Node or TypeScript.

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

Install

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.

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

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 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 for browser options.

Core resources

// 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

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 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 GETs and idempotent: true POSTs 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.

const controller = new AbortController();
setTimeout(() => controller.abort(), 500);
await hatched.buddies.get(buddyId, controller.signal);

Rate limits + request ids

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