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 lists every code.
The error envelope
A failed request answers with a 4xx or 5xx status and a body like this:
{
"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 |
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 |
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) |
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) |
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 |
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
- Python
- PHP
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.
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");
}
}
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
$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']);
}
}
}
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: every error code
- Rate limits
- Idempotency