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
| Code | HTTP | SDK class | Meaning |
|---|---|---|---|
bad_request | 400 | HatchedError (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_conflict | 400 | CategoryConflictError | An equip request tried to put two items in the same category slot (only accessory stacks). err.category / err.conflictingItemIds are on the error. |
insufficient_balance | 400 | InsufficientBalanceError | Buddy does not have enough coins/tokens for the spend. |
missing_audience | 400 | HatchedError (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_items | 400 | TooManyItemsError | 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. |
unknown_audience | 400 | HatchedError (base) | The audience value sent on the request is not one of the customer’s configured audiences. |
unauthorized | 401 | UnauthorizedError | The API key is missing, invalid, or revoked. |
credit_insufficient | 402 | CreditInsufficientError | No credit pool has enough credits for the requested AI job. err.required / err.available / err.topUpUrl / err.upgradeUrl are on the error. |
event_quota_exceeded | 402 | EventQuotaExceededError | The monthly event quota for the plan is exhausted. err.used / err.limit / err.resetAt / err.upgradeUrl are on the error. |
onboarding_cap_reached | 402 | HatchedError (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_required | 402 | HatchedError (base) | Generic 402 fallback when a billing block has no more specific code (parsed as a base HatchedError with code: "payment_required"). |
capability_disabled | 403 | HatchedError (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. |
forbidden | 403 | ForbiddenError | Key is valid but lacks permission for this endpoint. |
plan_feature_locked | 403 | PlanFeatureLockedError | The 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_scope | 403 | PublishableKeyScopeError | Publishable key cannot mutate — use a secret key server-side. |
widget_token_scope (reserved) | 403 | WidgetTokenScopeError | Reserved — not currently emitted by the API. The SDK defines WidgetTokenScopeError defensively for a future widget-token scope check. |
not_found | 404 | NotFoundError | Generic 404 fallback — a Nest route or guard threw a NotFoundException without the richer resource_not_found code (e.g. an unmatched route). |
resource_not_found | 404 | NotFoundError | The referenced id does not exist or was archived. |
active_egg_limit | 409 | ActiveEggLimitError | The per-user active-egg cap was reached. err.details.active lists the existing eggs (id + status). |
config_version_mismatch | 409 | ConfigVersionMismatchError | The buddy is pinned to a different config version than expected. |
conflict | 409 | ConflictError | A competing mutation won; retry is safe if you re-read state first. |
idempotency_key_conflict | 409 | ConflictError | An 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_config | 409 | NoPublishedConfigError | The customer has no published config version yet, so eggs cannot be created. |
validation_failed | 422 | ValidationError | 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). |
rate_limited | 429 | RateLimitError | Over the per-minute quota. Honour err.retryAfter (seconds). |
internal_server_error | 500 | HatchedError (base) | An unhandled error reached the global filter. The message is the thrown error message; no stable details shape. |
bad_gateway | 502 | HatchedError (base) | Generic 502 fallback when an upstream dependency failed without the more specific upstream_image_error code. |
upstream_image_error | 502 | UpstreamImageError | The art provider failed during hatch/evolve. No ledger writes happened. |
onboarding_extract_failed | 503 | HatchedError (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_unavailable | 503 | HatchedError (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
fromdate afterto, an unparseable filter). Request-body/DTO validation does NOT land here — that isvalidation_failed(422).err.detailsshape varies per endpoint. - Fix: Read
err.message/err.detailsto 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
accessorystacks).err.category/err.conflictingItemIdsare 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
audiencefield. 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.attemptedare on the error. - Fix: Unequip something first, then retry with at most
err.maxitems.
unknown_audience
- HTTP status: 400
- SDK class:
HatchedError(base) - Meaning: The
audiencevalue sent on the request is not one of the customer’s configured audiences. - Fix: Use one of the keys configured in Dashboard → Audiences (the
audiencemust 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.upgradeUrlare on the error. - Fix: Do NOT retry. Branch on
err.code; send the user todetails.top_up_urlordetails.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.upgradeUrlare on the error. - Fix: Do NOT retry. Branch on
err.code; back off untildetails.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_urlare on the error. - Fix: Do NOT retry. Branch on
err.code; send the operator todetails.upgrade_urlto 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
HatchedErrorwithcode: "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_planare on the error. - Fix: Do NOT retry. Enable the capability for the workspace (Dashboard → Settings), or branch on
err.codeand 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.upgradeUrlare on the error. - Fix: Do NOT retry. Branch on
err.codeand prompt an upgrade todetails.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
ErrorCodeenum.
- HTTP status: 403
- SDK class:
WidgetTokenScopeError(from@hatched/sdk-js) - Meaning: Reserved — not currently emitted by the API. The SDK defines
WidgetTokenScopeErrordefensively for a future widget-token scope check. - Fix: No action needed today; handle it the same as
forbiddenif 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
NotFoundExceptionwithout the richerresource_not_foundcode (e.g. an unmatched route). - Fix: Verify the URL and the id; both map to
NotFoundErrorin 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.activelists 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-Keywith a different body. The SDK surfaces this as a genericConflictErrorwithcode: "idempotency_key_conflict". - Fix: Use a fresh
Idempotency-Keyfor 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_urlpoints 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 thatSchema.parse(body)with Zod, and domain business-rule checks.err.details.fieldsis a consistent{ "<field path>": ["<message>", ...] }map across all three; Zod failures additionally includeerr.details.issues(the raw issue array). - Fix: Map
err.details.fieldsonto 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
messageis the thrown error message; no stabledetailsshape. - Fix: Not retryable as-is — capture
err.requestIdand 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_errorcode. - 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.reasonis 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;
}