Errores
Todos los errores se devuelven en un formato único: ok: false más un objeto error:
{ "ok": false, "error": { "code": "INVALID_API_KEY", "message": "Invalid API key" }}code: código legible por máquina enUPPER_SNAKE_CASE, estable, no cambia sin un bump de versión major. Haz el switch sobre él, no sobremessage.message: explicación legible por humanos, en inglés. El texto puede precisarse sin bump de versión.details: opcional. ParaINVALID_INPUTes un array{ path, message }con cada campo que falló. ParaINVALID_FIELDse añade a cada elementoexpectedcon el nombre canónico del campo.docs: opcional, enlace a la sección de esta página cuando hay una explicación más detallada del código.request_id: se añade solo a las respuestas5xxyBAD_JSON; cítalo al contactar con soporte.
Códigos HTTP
Sección titulada «Códigos HTTP»| Estado | Significado |
|---|---|
200 OK | Éxito |
400 Bad Request | JSON roto, campos que faltan o validación Zod fallida |
401 Unauthorized | X-Api-Key ausente o no válido |
402 Payment Required | Hace falta un plan o pack superior, o la suscripción no está activa |
403 Forbidden | La clave es válida, pero no tiene permisos (scope, origin, suspended) |
404 Not Found | El recurso, la clave o el job no existe |
422 Unprocessable Entity | La entrada es válida, pero el motor no pudo calcular |
429 Too Many Requests | Se ha superado el rate limit, se han agotado los créditos o el spend-cap |
500 Internal Server Error | Error interno, de nuestro lado; cita el request_id |
503 Service Unavailable | La pasarela de IA / la cola no están disponibles temporalmente |
Códigos de error
Sección titulada «Códigos de error»Autenticación (401)
Sección titulada «Autenticación (401)»| Código | Significado |
|---|---|
MISSING_API_KEY | No se ha enviado la cabecera X-Api-Key |
INVALID_API_KEY | La clave no existe o está revocada |
MISSING_BEARER | Se eligió el esquema Authorization: Bearer, pero falta el token |
WRONG_AUTH_METHOD | Se envió X-Api-Key a /v1/me/* (solo Bearer). Para comprobar una clave, GET /v1/auth/keys/me; para las llamadas a la API, cualquier /v1/* con X-Api-Key |
Permisos (403)
Sección titulada «Permisos (403)»| Código | Significado |
|---|---|
INSUFFICIENT_SCOPE | La clave no tiene permisos para esta acción |
DOMAIN_MISMATCH / FORBIDDEN_ORIGIN | El origin de la petición no está en la allow-list de la clave |
ENDPOINT_NOT_IN_SCOPE | La clave está limitada a una lista de endpoints (allowed_endpoints) y esta ruta no está en ella. details indica la lista |
KEY_SUSPENDED | La clave está suspendida |
Facturación (402)
Sección titulada «Facturación (402)»| Código | Significado |
|---|---|
PLAN_UPGRADE_REQUIRED | El endpoint requiere un plan superior o un pack add-on |
PLAN_PACK_MISMATCH | El endpoint forma parte de un pack que la clave no tiene |
SUBSCRIPTION_EXPIRED | El periodo pagado terminó hace más de 3 días. Durante los 3 primeros días tras el vencimiento las peticiones se siguen atendiendo según el plan y llevan la cabecera X-Subscription-Grace-Ends. details contiene expired_at, grace_ended_at y renew_url |
Rate limiting y créditos (429)
Sección titulada «Rate limiting y créditos (429)»| Código | Significado |
|---|---|
RATE_LIMIT | Se ha superado el límite de RPM. La respuesta contiene la cabecera Retry-After: <sec> |
CREDITS_EXHAUSTED | Se ha agotado el presupuesto mensual de créditos |
SPEND_CAP_REACHED | Se ha alcanzado el límite de gasto en endpoints de IA |
KEY_BUDGET_EXHAUSTED | La clave ha agotado su propio presupuesto (credits_cap_cycle). La cuenta puede seguir teniendo créditos: la restricción afecta solo a esta clave |
Validación (400)
Sección titulada «Validación (400)»| Código | Significado |
|---|---|
INVALID_INPUT | Uno o varios campos no han pasado la validación Zod. details es un array { path, message } |
INVALID_FIELD | El cuerpo contiene un nombre de campo corto en lugar del canónico. Ver más abajo |
MISSING_FIELDS | Falta un campo obligatorio |
BAD_JSON | JSON roto en el cuerpo de la petición |
Ejemplo de 400:
{ "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" } ] }}INVALID_FIELD
Sección titulada «INVALID_FIELD»Los endpoints de cartas solo aceptan nombres de campo completos. Las formas cortas se rechazan con 400, en lugar de leerse en silencio:
| Enviado | Campo canónico | Qué se espera |
|---|---|---|
lat | latitude | grados decimales, norte positivo |
lng, lon, long | longitude | grados decimales, este positivo |
tz con un número, tzOffset, utcOffset, timezone_offset | timezoneOffset | número de horas desde UTC, p. ej. 5.75 |
tz con un nombre, timeZone, time_zone | timezone | nombre de zona IANA, p. ej. Europe/Kyiv, o auto |
La respuesta enumera todos los campos encontrados de una vez, en el orden del cuerpo de la petición, así que el renombrado se hace en una sola pasada:
{ "ok": false, "error": { "code": "INVALID_FIELD", "message": "Unsupported fields: \"tz\", \"lat\", \"lng\". Rename them to \"timezoneOffset\", \"latitude\", \"longitude\". See details for what each one expects.", "details": [ { "path": "tz", "expected": "timezoneOffset", "message": "numeric hours from UTC, e.g. 5.75; a zone name goes in timezone" }, { "path": "lat", "expected": "latitude", "message": "decimal degrees, north positive" }, { "path": "lng", "expected": "longitude", "message": "decimal degrees, east positive" } ], "docs": "https://api.astroway.info/en/errors/#invalid_field" }}path indica la ruta completa, así que en synastry se ve de quién es la carta: chart1.lat, no solo lat.
El mismo código se devuelve cuando timezone no se puede interpretar: un valor vacío, una abreviatura como EST (que es un -5 fijo todo el año, así que Nueva York en julio saldría con una hora de error), un nombre que no está en la base de husos horarios, un desfase escrito como texto, como +03:00, o auto sin latitude ni longitude. El mensaje sugiere qué enviar en su lugar, y path apunta al objeto, por ejemplo chart2.timezone.
Dos cosas a tener en cuenta.
No es una deprecación. Los nombres cortos nunca formaron parte del contrato ni figuraron en /openapi.json como campos del cuerpo. Hasta el 2026-08-01 no funcionaban, sino que se ignoraban en silencio: las coordenadas tienen cero por defecto, así que la carta se calculaba para 0°N 0°E en UTC. El ascendente, las casas y todo lo que deriva de ellos eran incorrectos, y la respuesta no lo decía. Por eso las formas cortas no se vuelven a aceptar: dos nombres para un mismo campo para siempre, y además tz es ambiguo (número o nombre de zona).
Un point anidado es otra cosa. En /v1/acg-zones, /v1/acg/by-category, /v1/ccg-analysis y /v1/eclipse-analysis, el objeto point: { lat, lng } usa nombres de campo declarados, y se aceptan.
Coordenadas ausentes: 400 desde el 2026-11-09
Sección titulada «Coordenadas ausentes: 400 desde el 2026-11-09»El nombre de campo correcto es la mitad del asunto. Un cuerpo sin latitude ni longitude también daba 200, porque las coordenadas tenían cero por defecto, y la respuesta describía 0°N 0°E.
Hasta el 2026-11-09 una petición así funciona, pero la respuesta lleva Deprecation: true, Sunset, Warning y el campo _warning en el cuerpo. A partir del 2026-11-09 es 400 INVALID_INPUT con latitude is required, in decimal degrees.
latitude: 0, longitude: 0 explícitos siguen siendo válidos: el problema es el campo ausente, no el valor. timezoneOffset conserva el cero por defecto, porque ahí cero significa UTC. city no sustituye a las coordenadas: nadie lo geocodifica. La comprobación se aplica solo a los objetos que llevan a la vez date y time.
Errores de cálculo (422)
Sección titulada «Errores de cálculo (422)»| Código | Significado |
|---|---|
CALCULATION_ERROR | La entrada es válida, pero el motor no pudo calcular (por ejemplo, una latitud polar con Placidus). Prueba con Whole Sign / Equal o con otra fecha. |
Del servidor
Sección titulada «Del servidor»| Código | HTTP | Significado |
|---|---|---|
INTERNAL_ERROR | 500 | Error inesperado. Lo registramos con el stack trace; cita el request_id |
GATEWAY_UNAVAILABLE / AI_UNAVAILABLE / LLM_UNAVAILABLE | 503 | La pasarela de IA no está disponible. Reintenta con backoff exponencial |
QUEUE_UNAVAILABLE | 503 | La cola de tareas en segundo plano no está disponible |
Tratamiento en el código del cliente
Sección titulada «Tratamiento en el código del cliente»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})` : ''}`); }}Haz el switch sobre code, no sobre message. Los SDK oficiales (@astroway/sdk, astroway) lanzan subclases de error tipadas según el estado HTTP (AuthenticationError, RateLimitError, BadRequestError …), así que no tendrás que escribir el código de tratamiento.
Petición de soporte
Sección titulada «Petición de soporte»Para contactar, envía a support@astroway.info:
- El
request_idde la respuesta (si es un5xx) - El endpoint y el body JSON abreviado (sin secretos)
- Comportamiento esperado frente al real
- La zona horaria y la hora aproximada de la petición (UTC)
La respuesta llega dentro del SLA de tu plan (48 h Starter, 24 h Pro, propio para Enterprise).