AlchemyCode API docs

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

JSON
{
  "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"
  ]
}
CampoTipoDescripción
typestring (URI)requeridoIdentificador del tipo de problema. Apunta a la página de este catálogo.
titlestringrequeridoTítulo corto y estable del tipo de problema.
statusintegerrequeridoCódigo HTTP de la respuesta.
codestringrequeridoCódigo estable en MAYÚSCULAS. Úsalo para decidir en tu código, no el texto.
detailstringrequeridoExplicación específica del caso. Puede incluir el RFC o el campo que falló.
instancestringopcionalRuta donde ocurrió el problema.
request_idstringrequeridoIdentificador de la petición. Inclúyelo en cualquier reporte a soporte.
retryablebooleanrequeridoIndica si tiene sentido reintentar la misma petición.
docs_urlstringrequeridoEnlace a la documentación del código.
errorsarrayopcionalSó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.

JSON
{
  "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"}
  ]
}

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.

HTTPCódigoTítuloReintentableDetalle
401API_KEY_EXPIREDLlave de API expiradaNoLa llave superó su fecha de expiración.
401API_KEY_INVALIDLlave de API inválidaNoLa llave no existe o fue revocada. Genera una nueva en el panel.
401API_KEY_REVOKEDLlave de API revocadaNoLa llave fue revocada. Usa otra llave activa.
401AUTH_REQUIREDFalta autenticaciónNoIncluye `Authorization: Bearer ck_live_...` en la petición.
403ACCOUNT_SUSPENDEDCuenta suspendidaNoLa cuenta está suspendida. Contacta a equipo@alchemycode.dev.
403COMPANY_FORBIDDENEmpresa de otra cuentaNoLa empresa no pertenece a tu cuenta.
403SCOPE_FORBIDDENPermiso insuficienteNoLa llave no tiene el scope necesario para este recurso.
404COMPANY_NOT_FOUNDEmpresa no encontradaNoNo existe una empresa con ese id en tu cuenta.
404JOB_NOT_FOUNDTrabajo no encontradoNoNo existe un trabajo con ese id en tu cuenta.
404NOT_FOUNDRecurso no encontradoNoLa ruta solicitada no existe.
405METHOD_NOT_ALLOWEDMétodo no permitidoNoUsa el método HTTP documentado.
409COMPANY_DUPLICATEEmpresa duplicadaNoYa existe una empresa con ese RFC en tu cuenta.
409COMPANY_LIMIT_REACHEDLímite de empresasNoTu plan alcanzó el máximo de empresas. Solicita ampliación.
409COMPANY_SUSPENDEDEmpresa suspendidaNoReactiva la empresa antes de solicitar servicios.
409IDEMPOTENCY_CONFLICTIdempotencia en conflictoNoEl `Idempotency-Key` ya se usó con un cuerpo distinto.
409IDEMPOTENCY_IN_PROGRESSIdempotencia en procesoSíLa misma petición sigue ejecutándose. Reintenta en unos segundos.
409JOB_CANCELEDTrabajo canceladoNoEl trabajo fue cancelado.
409JOB_NOT_READYTrabajo en procesoSíEl trabajo aún no termina. Consulta de nuevo o espera el webhook.
409PORTAL_CAPTCHA_REQUIREDCAPTCHA requeridoNoEl portal pidió un CAPTCHA. Usa el endpoint de CAPTCHA o resuelve en el panel.
410JOB_EXPIREDTrabajo expiradoNoEl resultado expiró; vuelve a solicitarlo.
413PAYLOAD_TOO_LARGECarga demasiado grandeNoEl archivo o cuerpo excede el límite permitido.
415UNSUPPORTED_MEDIA_TYPEFormato no soportadoNoEnvía `Content-Type: application/json` o `multipart/form-data` según el endpoint.
422CREDENTIAL_INVALIDCredenciales rechazadasNoEl portal rechazó las credenciales. Verifícalas en el panel.
422CREDENTIAL_MISSINGCredenciales incompletasNoLa empresa no tiene configuradas las credenciales que este servicio requiere.
422FIEL_EXPIREDe.firma vencidaNoLa e.firma de la empresa está vencida. Carga una vigente.
422FILE_INVALIDArchivo inválidoNoEl archivo no es un certificado o llave válidos del SAT.
422FILE_REQUIREDArchivo faltanteNoAdjunta el archivo `.cer` (y `.key` si aplica).
422PASSWORD_WEAKContraseña débilNoLa contraseña no cumple la política mínima.
422RFC_INVALIDRFC inválidoNoEl RFC debe tener 12 (moral) o 13 (física) caracteres y estructura válida.
422VALIDATION_ERRORDatos inválidosNoRevisa los campos marcados en `errors`.
429QUOTA_EXCEEDEDCuota mensual agotadaNoAlcanzaste las consultas incluidas del periodo. Solicita ampliación.
429RATE_LIMITEDDemasiadas peticionesSíSuperaste el límite por minuto. Respeta `Retry-After`.
500INTERNAL_ERRORError internoSíOcurrió un error inesperado. Reporta el `request_id` a soporte.
501NOT_IMPLEMENTEDNo implementadoNoEl servicio existe pero no está habilitado para tu cuenta.
502PROVIDER_ERRORError del proveedorSíEl servicio de origen devolvió un error. Reintenta con backoff.
503PORTAL_UNAVAILABLEPortal no disponibleSíEl portal de origen no responde. Reintenta más tarde.
503SERVICE_UNAVAILABLEServicio no disponibleSíMantenimiento o saturación. Consulta https://status.434alchemycode.com.
504PROVIDER_TIMEOUTTiempo de espera agotadoSíEl proveedor no respondió en el tiempo máximo.

Política de reintentos

HTTPCuándoQué hacer
400 / 422Cuerpo o credenciales inválidas.Corregir la petición. No reintentar.
401 / 403Llave inválida, vencida, revocada o sin scope.Corregir la llave o los scopes. No reintentar.
404Recurso inexistente o de otra cuenta.Verificar el UUID. No reintentar.
409Conflicto de estado (job no listo, duplicado, idempotencia).Leer el detalle; reintentar sólo JOB_NOT_READY e IDEMPOTENCY_IN_PROGRESS.
410Job o invitación expirados.Solicitar de nuevo el recurso.
413Cuerpo o archivo demasiado grande.Reducir el payload. No reintentar.
429Rate limit o cuota agotada.Respetar Retry-After; para cuota, esperar el corte mensual.
500Error interno.Reintentar con backoff y reportar request_id.
502 / 503 / 504Portal o proveedor no disponible.Reintentar con backoff exponencial (1, 2, 4, 8 s).
Regla simple: reintenta sólo si 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)

PHP
$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

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

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)
    raise

cURL

Shell
# -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_id de la respuesta (o del header X-Request-Id).
  • El endpoint, método y hora aproximada (con zona horaria).
  • El code exacto y el cuerpo del problema, sin credenciales ni datos personales.
Nunca envíes la e.firma, su contraseña, tu API key completa ni datos personales de terceros. Con el request_id podemos rastrear la petición sin esos datos.