# Errors & rate limits

Errors come back as `{ "error": "…", "code"?: "…", …details }`. The sentence says what happened and what to do; the code is stable for programs. Nothing is charged for a refused request; the one exception is an AI model that used its whole token budget without answering (`budget_exhausted`, see AI Gateway).

## Status codes

| Status | Meaning |
| --- | --- |
| 400 | The request is wrong: a missing field, a bad value, or a SQL mistake (the database's own message, cleaned). |
| 401 | No credential, or it is invalid or revoked. |
| 402 | Money or plan: a plan count reached, the Balance or AI credit too low, or the spending limit reached. |
| 403 | The credential may not do this: role, read-only key, project key, owner-only money action, a paused or unverified account. |
| 404 | Not found — or it belongs to another workspace. |
| 409 | The resource is in the wrong state: still being created, disabled, or a delete blocked by money or work in flight. |
| 410 | A database that no longer exists on the edge. |
| 413 | Too large: an upload over the size cap, or AI input over 200,000 characters. |
| 422 | Refused on purpose: a suppressed email address, or an AI answer that used the whole token budget with nothing to show. |
| 429 | Too many requests. Wait Retry-After seconds. |
| 501 | Not available yet (for example, a Postgres database or an image media asset). |
| 502 | Something upstream failed: a delivery provider refused, or a delete could not finish (it is safe to retry). |
| 503 | Temporarily unavailable. Retry after the Retry-After header when there is one. |

## Codes

| Code | Status | Meaning and fields |
| --- | --- | --- |
| plan_limit | 402 | A plan count or the email quota is reached. `limit`, `current`. |
| insufficient_balance | 402 | The Balance does not cover a purchase. `price_usd`, `balance_usd`, `shortfall_usd`, `add_funds_url`, `card_on_file`. |
| ai_credits_exhausted | 402 | Included AI and the Balance are used up. `available_usd`, `resets_on`, `buy_url`. |
| ai_credits_insufficient | 402 | Some AI credit is left, but less than this request's estimate. Lower `max_tokens`, shorten the prompt, choose a cheaper model or add funds. |
| ai_request_too_large | 402 | One AI request may reserve at most $2 on hobby, $10 on pro, $10 on enterprise. Shorten it, cap the output or split the work. |
| spend_limit_reached | 402 | The workspace's monthly spending limit would be passed. `spend_limit_usd`, `spent_this_month_usd`, `limits_url`. |
| email_unverified | 402 / 403 | The workspace owner's email must be verified (Hobby vendor AI models, live payment links). |
| payouts_require_owner | 403 | Withdrawals, refunds, payout setup and adding or removing the card on file need the owner signed in to the dashboard. |
| ai_gateway_off / ai_model_off | 403 | The AI Gateway, or this model, is switched off for the workspace (Dashboard → AI Gateway). |
| origin_not_allowed | 403 | An auth pool refused a browser Origin that is not on its list. |
| browser_access_off | 403 | An auth pool's browser sign-in is turned off. |
| mail_paused / mail_suspended | 403 | Sending is paused by an admin, or suspended by the platform. |
| invalid_domain | 400 | A sending domain that is not a real domain you could own (a single word, a test domain, a Harakumo name). |
| invalid_from / invalid_reply_to | 400 | A From or Reply-To address that is not allowed; the sentence says which rule. |
| sender_name / reserved_subject | 400 | A sender name or subject that breaks the rules in Mail → Sender names; the sentence says which. |
| from_domain_not_added | 400 | A `from` address on a domain that is not a sending domain of this workspace. Add it first. |
| domain_claimed_elsewhere / domain_taken | 409 | The domain belongs to another workspace on Harakumo. |
| dns_hosting_off | 409 | A sending domain bought on Harakumo whose DNS hosting is off. `platform_option` says how to turn it on. |
| mail_not_ready | 409 | An auth pool cannot email codes: the app has no sender name recipients may see. Set one on the Mail page. |
| payments_disabled | 409 | The payment account is disabled; re-enable it on the project. |
| deletion_blocked | 409 | A delete is refused. `blockers[]` has one sentence per reason (earnings, a withdrawal, an open checkout, a running deploy, bought domains). |
| confirm_forfeit_required | 409 | Deleting a workspace would give up a positive Balance; send `confirmForfeit: true` to accept. |
| suppressed | 422 | The recipient is on the suppression list. |
| budget_exhausted | 422 | The model used its whole `max_tokens` budget without an answer. Those tokens are charged; raise `max_tokens`. |
| ai_request_refused | 400 / 413 | The model refused the request itself (a parameter, or its size). It was not sent to other models; nothing was charged. |
| rate_limited | 429 | A rate limit. `retry_after`; API quotas add `limit` and `window_seconds`. |
| ai_rate_limited | 429 | The AI per-minute limit. `limit`, `window_seconds`, `retry_after_seconds`. |
| daily_cap | 429 | The workspace's own daily email cap. |
| code_limit | 429 | An auth pool's codes asked for from the browser reached the pool's limit or the workspace's share of the email quota. Retry-After. |
| zone_limit / account_zone_limit | 429 | New DNS setups for today are used up: 10 per workspace, or 20 across the workspaces one person owns (that one carries `retry_after`). |
| delivery_failed | 502 | The mail provider refused the message; `message` has the reason. |
| provider_error | 502 | A sending domain could not be set up; nothing was saved. Try again in a minute. |
| ai_unavailable / ai_provider_error | 502 | No model could answer right now; nothing was charged. `harakumo-1` fails over between vendors. |
| zone_capacity | 503 | Harakumo is not accepting new domain setups for a while; nothing was set up. Buying a domain is not affected. |
| teardown_incomplete | 502 | A delete left some resources behind; the project is kept as teardown_failed. Delete again to retry. |
| processor_error | varies | The payment processor refused; the sentence says why. |

## Handling errors in code

**SDK**

```js
import Harakumo, { HarakumoError } from '@harakumo/sdk';

try {
  await hk.databases.query(dbId, 'SELECT 1');
} catch (err) {
  if (err instanceof HarakumoError) {
    if (err.status === 429) { /* wait err.body.retry_after seconds, then retry */ }
    if (err.body?.code === 'plan_limit') { /* show err.message to the person */ }
  }
  throw err;
}
```
