# Errors

Every error comes back in a single shape — `ok: false` plus an `error` object:

```json
{
  "ok": false,
  "error": {
    "code": "INVALID_API_KEY",
    "message": "Invalid API key"
  }
}
```

- `code` — machine-readable `UPPER_SNAKE_CASE` code, **stable**, never changes without a major bump. Switch on this, not on `message`.
- `message` — human-readable explanation in English. Wording may be refined without a bump.
- `details` — optional. For `INVALID_INPUT` it's an array of `{ path, message }` for each failed field.
- `request_id` — added only to `5xx` and `BAD_JSON` responses; quote it when contacting support.

## HTTP status codes

| Status | Meaning |
|---|---|
| `200 OK` | Success |
| `400 Bad Request` | Malformed JSON, missing fields, or failed Zod validation |
| `401 Unauthorized` | Missing or invalid `X-Api-Key` |
| `402 Payment Required` | A higher plan / add-on pack is required, or the subscription is inactive |
| `403 Forbidden` | Key is valid but lacks permission (scope, origin, suspended) |
| `404 Not Found` | Resource, key, or job does not exist |
| `422 Unprocessable Entity` | Input is valid but the engine could not compute it |
| `429 Too Many Requests` | Rate limit exceeded, credits exhausted, or spend cap hit |
| `500 Internal Server Error` | Our fault — quote `request_id` |
| `503 Service Unavailable` | AI gateway / queue temporarily down |

## Error codes

### Authentication (401)

| Code | Meaning |
|---|---|
| `MISSING_API_KEY` | The `X-Api-Key` header was not sent |
| `INVALID_API_KEY` | Key does not exist or was revoked |
| `MISSING_BEARER` | `Authorization: Bearer` scheme selected but token missing |

### Permissions (403)

| Code | Meaning |
|---|---|
| `INSUFFICIENT_SCOPE` | Key lacks permission for this action |
| `DOMAIN_MISMATCH` / `FORBIDDEN_ORIGIN` | Request origin is not in the key's allow-list |
| `KEY_SUSPENDED` | Key has been suspended |

### Billing (402)

| Code | Meaning |
|---|---|
| `PLAN_UPGRADE_REQUIRED` | Endpoint requires a higher plan or an add-on pack |
| `PLAN_PACK_MISMATCH` | Endpoint is in a pack the key doesn't have |
| `SUBSCRIPTION_EXPIRED` | Subscription cancelled or expired |

### Rate limiting & credits (429)

| Code | Meaning |
|---|---|
| `RATE_LIMIT` | RPM limit exceeded. Response carries a `Retry-After: <sec>` header |
| `CREDITS_EXHAUSTED` | Monthly credit budget used up |
| `SPEND_CAP_REACHED` | AI-endpoint spend cap reached |

### Validation (400)

| Code | Meaning |
|---|---|
| `INVALID_INPUT` | One or more fields failed Zod validation. `details` is an array of `{ path, message }` |
| `MISSING_FIELDS` | A required field is missing |
| `BAD_JSON` | Malformed JSON in the request body |

Example `400`:

```json
{
  "ok": false,
  "error": {
    "code": "INVALID_INPUT",
    "message": "Validation failed: date: Invalid input: expected string, received undefined",
    "details": [
      { "path": "date", "message": "Invalid input: expected string, received undefined" },
      { "path": "time", "message": "Invalid input: expected string, received undefined" }
    ]
  }
}
```

### Calculation errors (422)

| Code | Meaning |
|---|---|
| `CALCULATION_ERROR` | Input is valid but the engine could not compute (e.g. a polar latitude for Placidus). Try Whole Sign / Equal or a different date. |

### Server

| Code | HTTP | Meaning |
|---|---|---|
| `INTERNAL_ERROR` | 500 | Unexpected error. Logged with a stack trace — quote `request_id` |
| `GATEWAY_UNAVAILABLE` / `AI_UNAVAILABLE` / `LLM_UNAVAILABLE` | 503 | AI gateway down. Retry with exponential backoff |
| `QUEUE_UNAVAILABLE` | 503 | Background-job queue down |

## Handling in client code

```ts
type ApiError = {
  ok: false;
  error: {
    code: string;
    message: string;
    details?: unknown;
    request_id?: string;
  };
};

async function chart(input: ChartInput) {
  const res = await fetch('https://api.astroway.info/v1/chart', {
    method: 'POST',
    headers: {
      'X-Api-Key': process.env.ASTROWAY_API_KEY!,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(input),
  });

  const body = await res.json();
  if (!body.ok) {
    throw new AstrowayError(body.error.code, body.error.message, body.error.request_id);
  }

  return body.data;
}

class AstrowayError extends Error {
  constructor(
    public code: string,
    message: string,
    public requestId?: string,
  ) {
    super(`[${code}] ${message}${requestId ? ` (request_id=${requestId})` : ''}`);
  }
}
```

Switch on `code`, not on `message`. The official SDKs ([`@astroway/sdk`](https://www.npmjs.com/package/@astroway/sdk), [`astroway`](https://pypi.org/project/astroway/)) throw typed error subclasses by HTTP status (`AuthenticationError`, `RateLimitError`, `BadRequestError`, …), so you won't need to write this yourself.

## Contacting support

To open a ticket, email [support@astroway.info](mailto:support@astroway.info):

1. The `request_id` from the response (for `5xx`)
2. The endpoint and a trimmed JSON body (no secrets)
3. Expected vs actual behavior
4. Timezone and approximate request time (UTC)

Replies land within your plan's SLA (48 h Starter, 24 h Pro, custom for Enterprise).
