# Errors

Every Ha Tang API, from Email to Payments to the chat gateway, reports errors in the same JSON shape with a conventional HTTP status. This page explains that shape, lists the errors you are most likely to meet with what to do about each, and shows how to handle them in code. The [error reference](https://hatang.dev/docs/reference/errors.md) lists every code.

## The error envelope

A failed request answers with a `4xx` or `5xx` status and a body like this:

```json
{
  "statusCode": 422,
  "name": "validation_error",
  "message": "to: Invalid email address"
}
```

| Field | Type | Meaning |
|---|---|---|
| `statusCode` | number | The HTTP status, repeated in the body |
| `name` | string | A stable, machine-readable code. **Branch on this.** |
| `message` | string | An English sentence for people and logs. It may change at any time; do not parse it. |

For validation errors, `message` names the first field that failed, in the form `field: problem`.

## Status codes

| Status | Meaning | Retry? |
|---|---|---|
| `400` | The request conflicts with an earlier one (idempotency) | No, fix the request |
| `401` | No API key, or the key is not allowed to do this | No |
| `403` | The key is invalid, the module is off, or the action is not allowed in this state | No |
| `404` | The resource does not exist, or belongs to another business or environment | No |
| `409` | Conflict with the current state (a duplicate, a closed conversation, a concurrent request) | Sometimes, after reading the current state |
| `413` | The uploaded file is too large | No |
| `422` | The request body or query is invalid | No, fix the request |
| `429` | Rate limit or daily quota reached | Yes, after a delay (rate limit only) |
| `500`, `503` | Something failed on our side or at a provider | Yes, with backoff |

## Common errors

| `name` | Status | What it means | What to do |
|---|---|---|---|
| `validation_error` | 422 | A field is missing or invalid, or the body is not valid JSON | Read `message`, fix the request. Do not retry as is. |
| `validation_error` | 403 | Email only: the `from` domain is not verified | [Verify the domain](https://hatang.dev/docs/guides/email/domains.md) |
| `missing_api_key` | 401 | No `Authorization: Bearer` header | Send the key |
| `invalid_api_key` | 403 | The key is malformed, unknown or revoked | Check the key; create a new one if it was revoked |
| `restricted_api_key` | 401 | The key lacks the scope, or its domain or origin restriction forbids this | Use a key with the right [scopes](https://hatang.dev/docs/guides/getting-started/authentication.md#scopes) |
| `product_disabled` | 403 | The module is turned off for the business | Ask an owner or admin to turn it on; do not retry |
| `environment_mismatch` | 403 | A Payments action or key does not match the environment | Check `Hatang-Environment` and the key |
| `not_found` | 404 | No such resource for this business (and environment) | Check the id; it may belong to another business or environment |
| `invalid_idempotent_request` | 400 | The `Idempotency-Key` was used before with a different body | Use a new key for a new request ([Idempotency](https://hatang.dev/docs/guides/getting-started/idempotency.md)) |
| `concurrent_idempotent_requests` | 409 | Two requests with the same `Idempotency-Key` ran at the same time | Retry after a moment; you get the first result |
| `rate_limit_exceeded` | 429 | Too many requests per second for this module | Wait `retry-after` seconds and retry ([Rate limits](https://hatang.dev/docs/guides/getting-started/rate-limits.md)) |
| `daily_quota_exceeded` | 429 | The business reached its daily email quota | Wait for midnight UTC or raise the quota |
| `conversation_closed` | 409 | The chat conversation is closed | Reopen it, or start a new conversation |
| `file_too_large`, `unsupported_file_type` | 413, 422 | An uploaded file is too big or of a type that is not accepted | Check the [limits](https://hatang.dev/docs/reference/limits.md) |
| `invalid_state` | 409 | The object cannot do that now, such as canceling a paid payment | Fetch the object and check its status |
| `application_error` | 500 or 403 | An unexpected failure; also `403` when email sending is paused for the business | Retry a `500` with backoff; for `403`, resume sending in **Email → Email settings** |
| `internal_server_error` | 500 | Something broke on our side | Retry with backoff; contact support if it persists |
| `provider_unavailable` | 503 | A bank or e-invoice provider did not answer | Retry later |

## Handle errors in code

Decide what to do from `name` and `statusCode`: retry what is temporary, surface what a person must fix, and log the rest with the request.

**Node.js**

The Node SDK never throws for API errors. Every method resolves to `{ data, error }`, where `error` has the same `statusCode`, `name` and `message`. When the API cannot be reached at all, `error.statusCode` is `0` and `error.name` is `application_error`.

```js
import { Hatang } from "@hatang/sdk";

const hatang = new Hatang(process.env.HATANG_API_KEY, { baseUrl: "https://api.hatang.dev" });

const { data, error } = await hatang.emails.send({ from, to, subject, html }, { idempotencyKey: orderId });

if (error) {
  switch (error.name) {
    case "validation_error":
      // Our bug or bad input: log it and fix the payload.
      logger.error({ error }, "Hatang rejected the email");
      break;
    case "rate_limit_exceeded":
    case "internal_server_error":
    case "application_error":
      // Temporary: retry later with the same idempotency key.
      await queue.retryLater({ orderId });
      break;
    case "product_disabled":
    case "restricted_api_key":
    case "invalid_api_key":
      // Configuration: alert a human, do not retry.
      await alertOps(error.message);
      break;
    default:
      logger.error({ error }, "Unexpected Hatang error");
  }
}
```

**Python**

```python
from hatang import Hatang, HatangError

hatang = Hatang()  # reads HATANG_API_KEY

try:
    hatang.emails.send(payload, idempotency_key=order_id)
except HatangError as err:
    if err.retryable:                    # rate limited, 5xx or unreachable (status_code 0)
        retry_later(order_id)            # temporary
    elif err.name in ("product_disabled", "restricted_api_key", "invalid_api_key"):
        alert_ops(err.message)           # configuration
    else:
        raise ValueError(f"{err.name}: {err.message}")  # fix the request
```

**PHP**

```php
$res = $client->post('/emails', [
    'headers' => ['Idempotency-Key' => $orderId],
    'json' => $payload,
    'http_errors' => false,
]);

if ($res->getStatusCode() >= 400) {
    $err = json_decode((string) $res->getBody(), true);
    switch ($err['name'] ?? '') {
        case 'rate_limit_exceeded':
            retryLater($orderId);
            break;
        case 'product_disabled':
        case 'restricted_api_key':
        case 'invalid_api_key':
            alertOps($err['message']);
            break;
        default:
            if ($res->getStatusCode() >= 500) {
                retryLater($orderId);
            } else {
                throw new RuntimeException($err['name'] . ': ' . $err['message']);
            }
    }
}
```

> **Tip: Find failed calls**
>
> **Email → Logs** in the dashboard lists every API request of the business with its status, duration and the request and response bodies. Look for the calls with a `4xx` or `5xx` status to see the exact error each one returned.

## Next steps

- [Error reference](https://hatang.dev/docs/reference/errors.md): every error code
- [Rate limits](https://hatang.dev/docs/guides/getting-started/rate-limits.md)
- [Idempotency](https://hatang.dev/docs/guides/getting-started/idempotency.md)

Source: https://hatang.dev/docs/guides/getting-started/errors
