HatchedDocs
Pricing & billing

Handling 402 responses

credit_insufficient, event_quota_exceeded — what they mean and how to recover.

Hatched uses HTTP 402 Payment Required for two conditions the caller can fix by topping up or upgrading:

  • credit_insufficient — no pool has enough credits for the requested AI job.
  • event_quota_exceeded — the monthly event quota for the plan is exhausted.

Plus 403 plan_feature_locked when a plan doesn't include the requested feature at all (e.g. Free plan hitting /marketplace/*).

Envelope

All three errors share the canonical envelope:

{
  "error": {
    "code": "credit_insufficient",
    "message": "Not enough credits for this AI job (need 1, have 0).",
    "details": {
      "required": 1,
      "available": 0,
      "welcome": 0,
      "paid": 0,
      "promo": 0,
      "upgrade_url": "https://app.hatched.dev/dashboard/billing",
      "top_up_url":  "https://app.hatched.dev/dashboard/billing?action=top_up"
    },
    "requestId": "req_abc123"
  }
}

Do NOT retry

Neither 402 nor 403 are transient. Do not wrap them in exponential backoff. The SDK's built-in retry only kicks in for 429 and upstream 5xx; 402/403 are surfaced to the caller immediately.

Recover

  • credit_insufficient → send the user to details.upgrade_url or details.top_up_url (both open the Stripe portal).
  • event_quota_exceeded → back off until details.reset_at (first of next UTC month) or upgrade to a higher plan.
  • plan_feature_locked → prompt upgrade to details.required_plan.

SDK recipe

import {
  HatchedClient,
  CreditInsufficientError,
  EventQuotaExceededError,
  PlanFeatureLockedError,
} from '@hatched/sdk-js';

const hatched = new HatchedClient({ apiKey: process.env.HATCHED_SECRET_KEY! });

try {
  await hatched.events.send({ userId: 'u_1', type: 'lesson_completed' });
} catch (err) {
  if (err instanceof EventQuotaExceededError) {
    console.warn(
      `Event quota exceeded (${err.used}/${err.limit}). Resets ${err.resetAt}.`,
    );
    redirect(err.upgradeUrl!);
  } else if (err instanceof CreditInsufficientError) {
    redirect(err.topUpUrl ?? err.upgradeUrl!);
  } else if (err instanceof PlanFeatureLockedError) {
    showUpgradePrompt(err.requiredPlan, err.upgradeUrl);
  } else {
    throw err;
  }
}

Response headers

Every authenticated response includes credit / quota metadata in headers so you can warn the operator before they hit the wall:

  • X-Credits-Remaining, X-Credits-Welcome-Remaining, X-Credits-Paid-Remaining, X-Credits-Promo-Remaining
  • X-Event-Quota-Limit, X-Event-Quota-Used, X-Event-Quota-Remaining, X-Event-Quota-Reset-At
  • X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset

Webhook signal

When a customer crosses 80% of their monthly event quota we emit one usage.threshold_reached webhook event (limit_type: 'event_quota'). The 100% boundary is not webhooked — it is hard enforced via 402 and surfaced in the dashboard banner.