HatchedDocs
Reference

Error codes

Every stable error code Hatched raises — HTTP status, SDK class, and how to fix.

Every error response follows the canonical envelope:

{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry after 60s",
    "requestId": "req_abc_123",
    "details": { "...": "..." }
  }
}

The SDK parses this envelope and throws a typed subclass of HatchedError with code, statusCode, requestId, and details. The string codes match the ErrorCode enum exported from @hatched/sdk-js, so you can switch on err.code exhaustively.

Catalogue

CodeHTTPSDK classMeaning
bad_request400HatchedError (base)A generic domain bad-request thrown explicitly by a service or controller (e.g. a from date after to, an unparseable filter). Request-body/DTO validation does NOT land here — that is validation_failed (422). err.details shape varies per endpoint.
category_conflict400CategoryConflictErrorAn equip request tried to put two items in the same category slot (only accessory stacks). err.category / err.conflictingItemIds are on the error.
insufficient_balance400InsufficientBalanceErrorBuddy does not have enough coins/tokens for the spend.
missing_audience400HatchedError (base)The customer has 2+ audiences defined, but the request omitted the audience field. Single-audience customers never see this (the server applies the implicit default).
too_many_items400TooManyItemsErrorAn equip request asked the buddy to wear more items than the compositing pipeline can render (cap is four). err.max / err.attempted are on the error.
unknown_audience400HatchedError (base)The audience value sent on the request is not one of the customer’s configured audiences.
unauthorized401UnauthorizedErrorThe API key is missing, invalid, or revoked.
credit_insufficient402CreditInsufficientErrorNo credit pool has enough credits for the requested AI job. err.required / err.available / err.topUpUrl / err.upgradeUrl are on the error.
event_quota_exceeded402EventQuotaExceededErrorThe monthly event quota for the plan is exhausted. err.used / err.limit / err.resetAt / err.upgradeUrl are on the error.
onboarding_cap_reached402HatchedError (base)A free onboarding/trial cap was hit (images, chat turns, or session duration). err.details.cap / err.details.used / err.details.limit / err.details.upgrade_url are on the error.
payment_required402HatchedError (base)Generic 402 fallback when a billing block has no more specific code (parsed as a base HatchedError with code: "payment_required").
capability_disabled403HatchedError (base)A workspace-level capability flag for this feature is turned off, even though the plan would allow it. err.details.feature / err.details.current_plan are on the error.
forbidden403ForbiddenErrorKey is valid but lacks permission for this endpoint.
plan_feature_locked403PlanFeatureLockedErrorThe customer's plan does not include the requested feature (e.g. Free tier hitting /marketplace/*). err.requiredPlan / err.upgradeUrl are on the error.
publishable_key_scope403PublishableKeyScopeErrorPublishable key cannot mutate — use a secret key server-side.
widget_token_scope (reserved)403WidgetTokenScopeErrorReserved — not currently emitted by the API. The SDK defines WidgetTokenScopeError defensively for a future widget-token scope check.
not_found404NotFoundErrorGeneric 404 fallback — a Nest route or guard threw a NotFoundException without the richer resource_not_found code (e.g. an unmatched route).
resource_not_found404NotFoundErrorThe referenced id does not exist or was archived.
active_egg_limit409ActiveEggLimitErrorThe per-user active-egg cap was reached. err.details.active lists the existing eggs (id + status).
config_version_mismatch409ConfigVersionMismatchErrorThe buddy is pinned to a different config version than expected.
conflict409ConflictErrorA competing mutation won; retry is safe if you re-read state first.
idempotency_key_conflict409ConflictErrorAn earlier request reused this Idempotency-Key with a different body. The SDK surfaces this as a generic ConflictError with code: "idempotency_key_conflict".
no_published_config409NoPublishedConfigErrorThe customer has no published config version yet, so eggs cannot be created.
validation_failed422ValidationErrorRequest input failed validation (HTTP 422). Emitted for every validation path: class-validator DTO failures (via the global ValidationPipe), controllers that Schema.parse(body) with Zod, and domain business-rule checks. err.details.fields is a consistent { "<field path>": ["<message>", ...] } map across all three; Zod failures additionally include err.details.issues (the raw issue array).
rate_limited429RateLimitErrorOver the per-minute quota. Honour err.retryAfter (seconds).
internal_server_error500HatchedError (base)An unhandled error reached the global filter. The message is the thrown error message; no stable details shape.
bad_gateway502HatchedError (base)Generic 502 fallback when an upstream dependency failed without the more specific upstream_image_error code.
upstream_image_error502UpstreamImageErrorThe art provider failed during hatch/evolve. No ledger writes happened.
onboarding_extract_failed503HatchedError (base)The onboarding pipeline could not verify the gamification selections extracted from the generated plan. No state was persisted. err.details.reason is present when a reason was captured.
service_unavailable503HatchedError (base)A dependency is temporarily unavailable (generic 503 fallback).

bad_request

  • HTTP status: 400
  • SDK class: HatchedError (base)
  • Meaning: A generic domain bad-request thrown explicitly by a service or controller (e.g. a from date after to, an unparseable filter). Request-body/DTO validation does NOT land here — that is validation_failed (422). err.details shape varies per endpoint.
  • Fix: Read err.message / err.details to see which input was rejected; fix it and resend.
  • More: Guide →

category_conflict

  • HTTP status: 400
  • SDK class: CategoryConflictError (from @hatched/sdk-js)
  • Meaning: An equip request tried to put two items in the same category slot (only accessory stacks). err.category / err.conflictingItemIds are on the error.
  • Fix: Unequip the conflicting item before equipping the new one.

insufficient_balance

  • HTTP status: 400
  • SDK class: InsufficientBalanceError (from @hatched/sdk-js)
  • Meaning: Buddy does not have enough coins/tokens for the spend.
  • Fix: Check err.balance / err.required; surface to the user.

missing_audience

  • HTTP status: 400
  • SDK class: HatchedError (base)
  • Meaning: The customer has 2+ audiences defined, but the request omitted the audience field. Single-audience customers never see this (the server applies the implicit default).
  • Fix: Send a valid audience (the role/segment key) on the request. List the configured audiences in Dashboard → Audiences.
  • More: Guide →

too_many_items

  • HTTP status: 400
  • SDK class: TooManyItemsError (from @hatched/sdk-js)
  • Meaning: An equip request asked the buddy to wear more items than the compositing pipeline can render (cap is four). err.max / err.attempted are on the error.
  • Fix: Unequip something first, then retry with at most err.max items.

unknown_audience

  • HTTP status: 400
  • SDK class: HatchedError (base)
  • Meaning: The audience value sent on the request is not one of the customer’s configured audiences.
  • Fix: Use one of the keys configured in Dashboard → Audiences (the audience must match exactly).
  • More: Guide →

unauthorized

  • HTTP status: 401
  • SDK class: UnauthorizedError (from @hatched/sdk-js)
  • Meaning: The API key is missing, invalid, or revoked.
  • Fix: Double-check HATCHED_API_KEY; rotate in Dashboard → Developers if leaked.
  • More: Guide →

credit_insufficient

  • HTTP status: 402
  • SDK class: CreditInsufficientError (from @hatched/sdk-js)
  • Meaning: No credit pool has enough credits for the requested AI job. err.required / err.available / err.topUpUrl / err.upgradeUrl are on the error.
  • Fix: Do NOT retry. Branch on err.code; send the user to details.top_up_url or details.upgrade_url (Stripe portal).
  • More: Guide →

event_quota_exceeded

  • HTTP status: 402
  • SDK class: EventQuotaExceededError (from @hatched/sdk-js)
  • Meaning: The monthly event quota for the plan is exhausted. err.used / err.limit / err.resetAt / err.upgradeUrl are on the error.
  • Fix: Do NOT retry. Branch on err.code; back off until details.reset_at (first of next UTC month) or show an upgrade prompt.
  • More: Guide →

onboarding_cap_reached

  • HTTP status: 402
  • SDK class: HatchedError (base)
  • Meaning: A free onboarding/trial cap was hit (images, chat turns, or session duration). err.details.cap / err.details.used / err.details.limit / err.details.upgrade_url are on the error.
  • Fix: Do NOT retry. Branch on err.code; send the operator to details.upgrade_url to lift the onboarding cap.
  • More: Guide →

payment_required

  • HTTP status: 402
  • SDK class: HatchedError (base)
  • Meaning: Generic 402 fallback when a billing block has no more specific code (parsed as a base HatchedError with code: "payment_required").
  • Fix: Do NOT retry. Branch on err.code; surface upgrade / top-up to the operator.
  • More: Guide →

capability_disabled

  • HTTP status: 403
  • SDK class: HatchedError (base)
  • Meaning: A workspace-level capability flag for this feature is turned off, even though the plan would allow it. err.details.feature / err.details.current_plan are on the error.
  • Fix: Do NOT retry. Enable the capability for the workspace (Dashboard → Settings), or branch on err.code and hide the feature.

forbidden

  • HTTP status: 403
  • SDK class: ForbiddenError (from @hatched/sdk-js)
  • Meaning: Key is valid but lacks permission for this endpoint.
  • Fix: Check your plan tier or the key's scope.

plan_feature_locked

  • HTTP status: 403
  • SDK class: PlanFeatureLockedError (from @hatched/sdk-js)
  • Meaning: The customer's plan does not include the requested feature (e.g. Free tier hitting /marketplace/*). err.requiredPlan / err.upgradeUrl are on the error.
  • Fix: Do NOT retry. Branch on err.code and prompt an upgrade to details.required_plan.
  • More: Guide →

publishable_key_scope

  • HTTP status: 403
  • SDK class: PublishableKeyScopeError (from @hatched/sdk-js)
  • Meaning: Publishable key cannot mutate — use a secret key server-side.
  • Fix: Move the call to a server route. See Auth model.
  • More: Guide →

widget_token_scope

Reserved — not currently emitted. The API never returns this code today; it exists in the SDK for forward-compatibility. Listed here so the catalogue stays in sync with the SDK ErrorCode enum.

  • HTTP status: 403
  • SDK class: WidgetTokenScopeError (from @hatched/sdk-js)
  • Meaning: Reserved — not currently emitted by the API. The SDK defines WidgetTokenScopeError defensively for a future widget-token scope check.
  • Fix: No action needed today; handle it the same as forbidden if you want forward-compatibility.

not_found

  • HTTP status: 404
  • SDK class: NotFoundError (from @hatched/sdk-js)
  • Meaning: Generic 404 fallback — a Nest route or guard threw a NotFoundException without the richer resource_not_found code (e.g. an unmatched route).
  • Fix: Verify the URL and the id; both map to NotFoundError in the SDK.

resource_not_found

  • HTTP status: 404
  • SDK class: NotFoundError (from @hatched/sdk-js)
  • Meaning: The referenced id does not exist or was archived.
  • Fix: Verify the id from a recent list/create response.

active_egg_limit

  • HTTP status: 409
  • SDK class: ActiveEggLimitError (from @hatched/sdk-js)
  • Meaning: The per-user active-egg cap was reached. err.details.active lists the existing eggs (id + status).
  • Fix: Hatch or cancel one of the listed eggs, or retry the create with ?ensure=true (eggs.create({ ..., ensure: true })) to reuse one.
  • More: Guide →

config_version_mismatch

  • HTTP status: 409
  • SDK class: ConfigVersionMismatchError (from @hatched/sdk-js)
  • Meaning: The buddy is pinned to a different config version than expected.
  • Fix: Migrate the buddy or pin your write to its current config.

conflict

  • HTTP status: 409
  • SDK class: ConflictError (from @hatched/sdk-js)
  • Meaning: A competing mutation won; retry is safe if you re-read state first.
  • Fix: Re-fetch the resource and retry the mutation.

idempotency_key_conflict

  • HTTP status: 409
  • SDK class: ConflictError (from @hatched/sdk-js)
  • Meaning: An earlier request reused this Idempotency-Key with a different body. The SDK surfaces this as a generic ConflictError with code: "idempotency_key_conflict".
  • Fix: Use a fresh Idempotency-Key for the new request, or replay the exact same body to receive the cached response.
  • More: Guide →

no_published_config

  • HTTP status: 409
  • SDK class: NoPublishedConfigError (from @hatched/sdk-js)
  • Meaning: The customer has no published config version yet, so eggs cannot be created.
  • Fix: Publish the gamification plan first. err.details.publish_url points at the dashboard publish page.
  • More: Guide →

validation_failed

  • HTTP status: 422
  • SDK class: ValidationError (from @hatched/sdk-js)
  • Meaning: Request input failed validation (HTTP 422). Emitted for every validation path: class-validator DTO failures (via the global ValidationPipe), controllers that Schema.parse(body) with Zod, and domain business-rule checks. err.details.fields is a consistent { "<field path>": ["<message>", ...] } map across all three; Zod failures additionally include err.details.issues (the raw issue array).
  • Fix: Map err.details.fields onto your form/inputs, fix the offending values, and resend.
  • More: Guide →

rate_limited

  • HTTP status: 429
  • SDK class: RateLimitError (from @hatched/sdk-js)
  • Meaning: Over the per-minute quota. Honour err.retryAfter (seconds).
  • Fix: Let the SDK retry (default on) or backoff manually.
  • More: Guide →

internal_server_error

  • HTTP status: 500
  • SDK class: HatchedError (base)
  • Meaning: An unhandled error reached the global filter. The message is the thrown error message; no stable details shape.
  • Fix: Not retryable as-is — capture err.requestId and report it.

bad_gateway

  • HTTP status: 502
  • SDK class: HatchedError (base)
  • Meaning: Generic 502 fallback when an upstream dependency failed without the more specific upstream_image_error code.
  • Fix: Transient upstream failure — retry with backoff.

upstream_image_error

  • HTTP status: 502
  • SDK class: UpstreamImageError (from @hatched/sdk-js)
  • Meaning: The art provider failed during hatch/evolve. No ledger writes happened.
  • Fix: Re-call the operation; idempotent.
  • More: Guide →

onboarding_extract_failed

  • HTTP status: 503
  • SDK class: HatchedError (base)
  • Meaning: The onboarding pipeline could not verify the gamification selections extracted from the generated plan. No state was persisted. err.details.reason is present when a reason was captured.
  • Fix: Retry plan generation after a short backoff.

service_unavailable

  • HTTP status: 503
  • SDK class: HatchedError (base)
  • Meaning: A dependency is temporarily unavailable (generic 503 fallback).
  • Fix: Transient — retry with backoff.

Programmatic handling

import {
  HatchedError,
  ValidationError,
  RateLimitError,
  InsufficientBalanceError,
  CreditInsufficientError,
  EventQuotaExceededError,
  PlanFeatureLockedError,
} from '@hatched/sdk-js';

try {
  await hatched.buddies.spend(buddyId, { amount: 100, reason: "item" });
} catch (err) {
  if (err instanceof InsufficientBalanceError) {
    return showInsufficientFunds(err.balance, err.required);
  }
  if (err instanceof ValidationError) {
    return showFieldErrors(err.details);
  }
  if (err instanceof RateLimitError) {
    return retryLater(err.retryAfter);
  }
  // 402 billing family: never retry — branch on the code and show upgrade / top-up.
  if (err instanceof CreditInsufficientError) {
    return redirectToBilling(err.topUpUrl ?? err.upgradeUrl);
  }
  if (err instanceof EventQuotaExceededError) {
    return showQuotaWall(err.used, err.limit, err.resetAt, err.upgradeUrl);
  }
  if (err instanceof PlanFeatureLockedError) {
    return showUpgradePrompt(err.requiredPlan, err.upgradeUrl);
  }
  if (err instanceof HatchedError) {
    console.error(err.code, err.requestId, err.message);
  }
  throw err;
}