Errores
Todos los errores usan application/problem+json (RFC 9457). El campo
code es estable y forma parte del contrato: no cambia entre versiones. detail es
texto para humanos y puede variar; request_id sirve para rastrear el caso en soporte.
Formato del problema
{
"type": "https://api.434alchemycode.com/docs/errors/credential_missing",
"title": "Credenciales incompletas",
"status": 422,
"code": "CREDENTIAL_MISSING",
"detail": "La empresa OMX2503255F9 no tiene configurado: e.firma (.cer/.key/contraseña). Cárgalas en POST /v1/companies/{id}/credentials.",
"instance": "/v1/sat/constancias",
"request_id": "req_8f2c1a4b7d9e3f60",
"retryable": false,
"docs_url": "https://api.434alchemycode.com/docs/errores/credential_missing",
"missing": [
"e.firma (.cer/.key/contraseña)"
],
"company_id": "9b2f5c1e-7a3d-4f8b-9c21-6d4e8a0b5f17",
"required": [
"fiel"
]
}| Campo | Tipo | Descripción |
|---|---|---|
type | string (URI)requerido | Identificador del tipo de problema. Apunta a la página de este catálogo. |
title | stringrequerido | Título corto y estable del tipo de problema. |
status | integerrequerido | Código HTTP de la respuesta. |
code | stringrequerido | Código estable en MAYÚSCULAS. Úsalo para decidir en tu código, no el texto. |
detail | stringrequerido | Explicación específica del caso. Puede incluir el RFC o el campo que falló. |
instance | stringopcional | Ruta donde ocurrió el problema. |
request_id | stringrequerido | Identificador de la petición. Inclúyelo en cualquier reporte a soporte. |
retryable | booleanrequerido | Indica si tiene sentido reintentar la misma petición. |
docs_url | stringrequerido | Enlace a la documentación del código. |
errors | arrayopcional | Sólo en VALIDATION_ERROR: lista de field, code y message. |
Algunos errores agregan campos de contexto: required_scope y key_scopes en
SCOPE_FORBIDDEN; missing, company_id y required en
CREDENTIAL_MISSING; quota en QUOTA_EXCEEDED; valid_scopes o
valid_events en errores de validación.
Errores de validación
Cuando el cuerpo no cumple el esquema, la API responde 422 VALIDATION_ERROR con un arreglo
errors. Cada entrada trae el campo, un código y un mensaje con la corrección.
{
"type": "https://api.434alchemycode.com/docs/errors/validation_error",
"title": "Datos inválidos",
"status": 422,
"code": "VALIDATION_ERROR",
"detail": "Revisa los campos marcados en `errors`.",
"instance": "/v1/companies",
"request_id": "req_2b7e9d1c4a6f8035",
"retryable": false,
"docs_url": "https://api.434alchemycode.com/docs/errores/validation_error",
"errors": [
{"field": "rfc", "code": "VALUE_ERROR", "message": "RFC_INVALID"}
]
}Catálogo completo
Este catálogo se genera directamente del código del backend: es la lista vigente, no una transcripción. Ordenado por código HTTP y luego por código de error.
| HTTP | Código | Título | Reintentable | Detalle |
|---|---|---|---|---|
| 401 | API_KEY_EXPIRED | Llave de API expirada | No | La llave superó su fecha de expiración. |
| 401 | API_KEY_INVALID | Llave de API inválida | No | La llave no existe o fue revocada. Genera una nueva en el panel. |
| 401 | API_KEY_REVOKED | Llave de API revocada | No | La llave fue revocada. Usa otra llave activa. |
| 401 | AUTH_REQUIRED | Falta autenticación | No | Incluye `Authorization: Bearer ck_live_...` en la petición. |
| 403 | ACCOUNT_SUSPENDED | Cuenta suspendida | No | La cuenta está suspendida. Contacta a equipo@alchemycode.dev. |
| 403 | COMPANY_FORBIDDEN | Empresa de otra cuenta | No | La empresa no pertenece a tu cuenta. |
| 403 | SCOPE_FORBIDDEN | Permiso insuficiente | No | La llave no tiene el scope necesario para este recurso. |
| 404 | COMPANY_NOT_FOUND | Empresa no encontrada | No | No existe una empresa con ese id en tu cuenta. |
| 404 | JOB_NOT_FOUND | Trabajo no encontrado | No | No existe un trabajo con ese id en tu cuenta. |
| 404 | NOT_FOUND | Recurso no encontrado | No | La ruta solicitada no existe. |
| 405 | METHOD_NOT_ALLOWED | Método no permitido | No | Usa el método HTTP documentado. |
| 409 | COMPANY_DUPLICATE | Empresa duplicada | No | Ya existe una empresa con ese RFC en tu cuenta. |
| 409 | COMPANY_LIMIT_REACHED | Límite de empresas | No | Tu plan alcanzó el máximo de empresas. Solicita ampliación. |
| 409 | COMPANY_SUSPENDED | Empresa suspendida | No | Reactiva la empresa antes de solicitar servicios. |
| 409 | IDEMPOTENCY_CONFLICT | Idempotencia en conflicto | No | El `Idempotency-Key` ya se usó con un cuerpo distinto. |
| 409 | IDEMPOTENCY_IN_PROGRESS | Idempotencia en proceso | Sí | La misma petición sigue ejecutándose. Reintenta en unos segundos. |
| 409 | JOB_CANCELED | Trabajo cancelado | No | El trabajo fue cancelado. |
| 409 | JOB_NOT_READY | Trabajo en proceso | Sí | El trabajo aún no termina. Consulta de nuevo o espera el webhook. |
| 409 | PORTAL_CAPTCHA_REQUIRED | CAPTCHA requerido | No | El portal pidió un CAPTCHA. Usa el endpoint de CAPTCHA o resuelve en el panel. |
| 410 | JOB_EXPIRED | Trabajo expirado | No | El resultado expiró; vuelve a solicitarlo. |
| 413 | PAYLOAD_TOO_LARGE | Carga demasiado grande | No | El archivo o cuerpo excede el límite permitido. |
| 415 | UNSUPPORTED_MEDIA_TYPE | Formato no soportado | No | Envía `Content-Type: application/json` o `multipart/form-data` según el endpoint. |
| 422 | CREDENTIAL_INVALID | Credenciales rechazadas | No | El portal rechazó las credenciales. Verifícalas en el panel. |
| 422 | CREDENTIAL_MISSING | Credenciales incompletas | No | La empresa no tiene configuradas las credenciales que este servicio requiere. |
| 422 | FIEL_EXPIRED | e.firma vencida | No | La e.firma de la empresa está vencida. Carga una vigente. |
| 422 | FILE_INVALID | Archivo inválido | No | El archivo no es un certificado o llave válidos del SAT. |
| 422 | FILE_REQUIRED | Archivo faltante | No | Adjunta el archivo `.cer` (y `.key` si aplica). |
| 422 | PASSWORD_WEAK | Contraseña débil | No | La contraseña no cumple la política mínima. |
| 422 | RFC_INVALID | RFC inválido | No | El RFC debe tener 12 (moral) o 13 (física) caracteres y estructura válida. |
| 422 | VALIDATION_ERROR | Datos inválidos | No | Revisa los campos marcados en `errors`. |
| 429 | QUOTA_EXCEEDED | Cuota mensual agotada | No | Alcanzaste las consultas incluidas del periodo. Solicita ampliación. |
| 429 | RATE_LIMITED | Demasiadas peticiones | Sí | Superaste el límite por minuto. Respeta `Retry-After`. |
| 500 | INTERNAL_ERROR | Error interno | Sí | Ocurrió un error inesperado. Reporta el `request_id` a soporte. |
| 501 | NOT_IMPLEMENTED | No implementado | No | El servicio existe pero no está habilitado para tu cuenta. |
| 502 | PROVIDER_ERROR | Error del proveedor | Sí | El servicio de origen devolvió un error. Reintenta con backoff. |
| 503 | PORTAL_UNAVAILABLE | Portal no disponible | Sí | El portal de origen no responde. Reintenta más tarde. |
| 503 | SERVICE_UNAVAILABLE | Servicio no disponible | Sí | Mantenimiento o saturación. Consulta https://status.434alchemycode.com. |
| 504 | PROVIDER_TIMEOUT | Tiempo de espera agotado | Sí | El proveedor no respondió en el tiempo máximo. |
Política de reintentos
| HTTP | Cuándo | Qué hacer |
|---|---|---|
| 400 / 422 | Cuerpo o credenciales inválidas. | Corregir la petición. No reintentar. |
| 401 / 403 | Llave inválida, vencida, revocada o sin scope. | Corregir la llave o los scopes. No reintentar. |
| 404 | Recurso inexistente o de otra cuenta. | Verificar el UUID. No reintentar. |
| 409 | Conflicto de estado (job no listo, duplicado, idempotencia). | Leer el detalle; reintentar sólo JOB_NOT_READY e IDEMPOTENCY_IN_PROGRESS. |
| 410 | Job o invitación expirados. | Solicitar de nuevo el recurso. |
| 413 | Cuerpo o archivo demasiado grande. | Reducir el payload. No reintentar. |
| 429 | Rate limit o cuota agotada. | Respetar Retry-After; para cuota, esperar el corte mensual. |
| 500 | Error interno. | Reintentar con backoff y reportar request_id. |
| 502 / 503 / 504 | Portal o proveedor no disponible. | Reintentar con backoff exponencial (1, 2, 4, 8 s). |
retryable es true o el HTTP es 429 con Retry-After. Todo lo demás se corrige, no se reintenta.Los jobs asíncronos ya reintentan solos hasta 3 veces los fallos del portal. Si el job termina en
failed, no vuelvas a consultar el mismo job: solicita uno nuevo cuando tenga sentido.
Manejo por lenguaje
PHP (cURL)
$status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$body = json_decode((string) $raw, true);
if ($status >= 400) {
$code = $body['code'] ?? 'UNKNOWN';
$detail = $body['detail'] ?? $body['title'] ?? 'Error desconocido';
$requestId = $body['request_id'] ?? 'n/a';
error_log("[{$code}] {$detail} (HTTP {$status}, request_id {$requestId})");
if ($code === 'RATE_LIMITED') {
$retryAfter = (int) ($body['retry_after'] ?? 5);
sleep(max(1, $retryAfter));
// reintenta la misma petición
} elseif ($body['retryable'] ?? false) {
// backoff exponencial
} else {
// error de negocio: no reintentes
}
}Node.js
const data = await response.json().catch(() => ({}));
if (!response.ok) {
console.error(`[${data.code ?? 'UNKNOWN'}] ${data.detail ?? response.statusText}`, {
status: response.status,
requestId: data.request_id,
retryable: data.retryable,
});
if (data.code === 'RATE_LIMITED') {
const seconds = Number(response.headers.get('retry-after') ?? 5);
await new Promise((r) => setTimeout(r, seconds * 1000));
}
throw new Error(data.code ?? `HTTP_${response.status}`);
}Python
try:
response = session.post(url, json=payload, timeout=(10, 45))
response.raise_for_status()
except requests.HTTPError as exc:
problem = exc.response.json()
print(problem["code"], problem.get("detail"), problem.get("request_id"))
if problem.get("retryable") or exc.response.status_code == 429:
wait = float(exc.response.headers.get("Retry-After", 5))
time.sleep(wait)
raisecURL
# -i muestra el status HTTP; el cuerpo de error viene en problem+json.
curl -i -sS -X POST "https://api.434alchemycode.com/v1/sat/constancias" \
-H "Authorization: Bearer $ALCHEMY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"company_id": "no-es-un-uuid"}'
# HTTP/1.1 422 Unprocessable Entity
# {"code":"VALIDATION_ERROR","detail":"Revisa los campos marcados en `errors`.","request_id":"req_...","errors":[...]}Cómo pedir ayuda
Escribe a equipo@alchemycode.dev con:
- El
request_idde la respuesta (o del headerX-Request-Id). - El endpoint, método y hora aproximada (con zona horaria).
- El
codeexacto y el cuerpo del problema, sin credenciales ni datos personales.
request_id podemos rastrear la petición sin esos datos.