factadesarrolladores API 0.1.0-esqueleto

facta-dte/api — SDK de PHP

English version

Un cliente de PHP para la API de Facta. Emite, consulta, anula y descarga sin que usted arme cabeceras, repita peticiones a mano ni escriba el código de reintentos. No firma y no calcula impuestos: el correlativo, los totales, el IVA, la firma y la transmisión a Hacienda siguen siendo del servidor, como en el cliente de TypeScript.

Esta primera versión cubre todo lo que necesita solo HTTP: emitir, preparar y firmar por separado, consultar, listar, descargar el Archivo DTE y el PDF, anular, eventos de retorno, entrega por correo y WhatsApp, el almacenamiento de Facta y la lectura y escritura del catálogo. Lo que necesita la llave de desbloqueo y almacenamiento local está en Lo que esta versión todavía no incluye.

Requisitos

  • PHP 7.4 o superior, y sin avisos de obsolescencia hasta PHP 8.5. Sirve el hosting compartido de siempre.
  • Ninguna extensión obligatoria más allá del núcleo de PHP (json y hash), ni dependencias de Composer.
  • HTTP: usa ext-curl cuando existe. Si su PHP no la trae, usa los flujos nativos de PHP (allow_url_fopen, y ext-openssl para HTTPS). Si ya tiene un cliente PSR-18, puede entregárselo con la opción transport.
  • ext-openssl también servirá para abrir el vault local en versiones futuras; hoy no hace falta para nada más que HTTPS sin cURL.
  • TLS nunca se desactiva. No hay una opción para saltarse la verificación. Si el servidor tiene un almacén de certificados viejo, la salida es caBundle: la ruta de un paquete de certificados actual.
  • Una llave de API (facta_test_… o facta_live_…) y, para emitir y anular, la llave de firma (factask_…). Se acuñan en la app, en Cuenta → API.

Instalación

Hay dos caminos, y los dos dejan el mismo cliente.

Con Composer (Packagist)

Es el camino principal. El paquete facta-dte/api está en Packagist:

sh
composer require facta-dte/api
phpsu script
require __DIR__ . '/vendor/autoload.php';

Sin Composer: el ZIP por FTP

Para hosting compartido, cPanel o cualquier servidor donde no tiene consola. Descargue facta-api-php-vX.Y.Z.zip de la página de versiones del repositorio, descomprímalo y suba por FTP la carpeta facta-api-php/ junto a su script (o a cualquier sitio que PHP pueda leer). Después:

phpsu script
require_once __DIR__ . '/facta-api-php/autoload.php';

No hay nada más que instalar. Si solo tiene Composer en otra máquina, ejecute allí composer require facta-dte/api y suba la carpeta vendor/ que genera.

Credenciales y opciones

Cree el cliente una vez por petición con las llaves que le dio la app. Léalas del gestor de secretos o del entorno del servidor, no de un archivo dentro del repositorio.

phpCrear el cliente
use Facta\Api\Facta;

$facta = new Facta([
    'apiKey'  => getenv('FACTA_API_KEY'),   // facta_test_… (ambiente 00) o facta_live_… (01)
    'signKey' => getenv('FACTA_SIGN_KEY'),  // factask_…, solo viaja a las rutas que firman
]);
OpciónQué hace
apiKeyObligatoria. facta_test_… emite en pruebas contra el ambiente 00; facta_live_…, en producción. Con otro prefijo el constructor lanza invalid_api_key.
signKeyfactask_…. Se manda solo a las rutas que firman: emitir, firmar, anular y registrar un retorno. Si le pasa por error una factauk_, el constructor la rechaza antes de que viaje.
unlockKeyfactauk_…. Reservada para la siguiente etapa (catálogo local y destinos propios). Nunca se envía al servidor; hoy solo cambia el mensaje cuando un catálogo cifrado no se puede leer.
baseUrlLa raíz de la API. Por defecto, la URL pública. Una llave facta_test_ sigue emitiendo en pruebas con esa misma URL.
timeoutMsPlazo de un intento, por defecto 60000. El Ministerio puede tardar unos cuarenta segundos en contestar.
maxRetriesIntentos extra para los errores donde reintentar es seguro, por defecto 3.
regionLa región de las funciones, enviada como x-region; false no manda ninguna. Por defecto, la variable de entorno FACTA_API_REGION y, si no existe, la que trae el cliente.
transportUn objeto Facta\Api\Http\Transport, por ejemplo new Psr18Transport($cliente, $fabricaDePeticiones, $fabricaDeFlujos).
caBundleRuta de un paquete de certificados, para servidores con el almacén desactualizado. La verificación TLS no se desactiva nunca.

Todos los métodos aceptan además, como último argumento, opciones por llamada: timeoutMs y headers; los que escriben, también idempotencyKey. Una opción que el método no conoce lanza InvalidArgumentException: un error de escritura no se ignora en silencio. Lo mismo vale para una opción desconocida del constructor.

Las respuestas llegan como arreglos asociativos con los nombres de campo de la propia API (estado, codigoGeneracion, numeroControl…). Las peticiones también son arreglos: una lista se convierte en un arreglo JSON, un arreglo asociativo en un objeto, y new \stdClass() es el objeto vacío {}.

Su primera factura

Una factura (01) de pruebas. Cambiar de ambiente es cambiar de llave, no de código.

phpemitir.php
require_once __DIR__ . '/facta-api-php/autoload.php';

use Facta\Api\Facta;
use Facta\Api\FactaError;

$facta = new Facta([
    'apiKey'  => getenv('FACTA_API_KEY'),
    'signKey' => getenv('FACTA_SIGN_KEY'),
]);

try {
    $resultado = $facta->issue(
        [
            'tipoDte' => '01',
            'items'   => [['descripcion' => 'Servicio', 'cantidad' => 1, 'precioUni' => 11.30]],
        ],
        ['idempotencyKey' => 'pedido-00417']   // suya: la misma venta, la misma llave
    );

    if ($resultado['estado'] === 'sellado') {
        echo $resultado['numeroControl'], ' ', $resultado['selloRecibido'], "\n";
    } else {
        echo "Contingencia: aceptado, falta el sello de Hacienda.\n";
    }
} catch (FactaError $e) {
    echo $e->getErrorCode(), ' (HTTP ', $e->getStatus(), '): ', $e->getMessage(), "\n";
    if ($e->isRejection()) {
        print_r($e->mhObservations());
    }
}

Tres cosas que ese ejemplo ya hace bien. La idempotencyKey es de su sistema (el número de pedido, el ticket de caja): si el proceso se cae a mitad de la llamada y usted la repite, el servidor reconoce la misma operación y no emite dos veces. El estado se mira antes de leer el sello, porque un documento en contingencia todavía no lo tiene. Y el fallo se trata por su código, no por su mensaje.

Métodos del cliente

Cada método dice qué alcance de la llave exige, qué devuelve y qué errores propios tiene. Los errores comunes a todos —llave, alcance, límites, red— están en Errores. Un método que escribe y gasta un correlativo lleva la Idempotency-Key del cliente; el apartado de idempotencia explica cómo funciona.

Configuración y estado

environment()

No hace ninguna petición. Devuelve "00" si la llave es facta_test_… y "01" si es facta_live_….

region()

No hace ninguna petición. Devuelve la región que se manda como x-region, o null si la desactivó con region => false. A diferencia del cliente de TypeScript, no consulta /v1/status para descubrirla: un proceso de PHP vive lo que dura una petición web, y una consulta por petición costaría más de lo que ahorra.

servedRegion()

Devuelve la región que sirvió la última respuesta (cabecera x-sb-edge-region), o null antes de la primera respuesta o cuando la cabecera no vino.

status($options = [])

GET /v1/status. No exige alcance ni manda la llave de firma. Confirma que la llave sirve y devuelve, como arreglo, ok, version, ambiente, emisor, llave (con alcances, tiposDte y catalogMode), firma, sincronizacion y limites. Es lo primero que conviene llamar; la ruta tiene su propia ventana de límite y no gasta el cupo que reporta.

php
$estado = $facta->status();
echo $estado['ambiente'], ' ', implode(',', $estado['llave']['alcances']), "\n";

diagnose($options = [])

Lee /v1/status y /v1/storage/status y responde si esta llave está lista. No abre vaults, no reserva un correlativo y no emite. Opciones: expectedEnvironment ("00" o "01"), requiredScopes (lista) y dteType (por ejemplo "01"). Devuelve overall, canIssue, canQuery, canDownload, canIssueAndArchive, la lista checks (cada una con id, state y message), storageReady, catalogMode, region y servedRegion. Si el estado no se puede leer, no lanza: devuelve un informe bloqueado con el código seguro del error.

canIssueAndArchive es siempre false en esta versión, porque el archivo local no existe todavía; y las comprobaciones que dependerían de almacenamiento local salen como unknown con ese motivo.

php
$informe = $facta->diagnose(['dteType' => '03']);
if (!$informe['canIssue']) {
    foreach ($informe['checks'] as $c) { echo $c['state'], ': ', $c['message'], "\n"; }
}

Emitir

issue($request, $options = [])

POST /v1/dte. Exige el alcance issue y manda la llave de firma. Prepara, firma y transmite en una sola operación; es el camino normal. $request lleva tipoDte, el receptor y las items, con los nombres de campo de la API; lo calcula todo el servidor. Devuelve el documento sellado (estado "sellado", con selloRecibido, totales y archivoDte) o, si Hacienda no contestó, un documento en contingencia (estado "contingencia", sin sello todavía) —un 202 no es un error—. La respuesta trae además codigoGeneracion, numeroControl, representacionGrafica (el PDF en base64), entrega, storage y advertencias cuando existen.

Opciones por llamada:

  • idempotencyKey: la suya. Si no la pasa, el cliente acuña una por llamada y la reutiliza en todos los reintentos.
  • deliver: marca canales de entrega; ver Entrega.
  • timeoutMs y headers.

Errores propios habituales: mh_rejected (Hacienda leyó y negó; gastó correlativo), validation_failed (no gastó), amount_limit, sign_key_required, sign_key_invalid, idempotency_key_reuse. Una emisión puede terminar con advertencias aunque haya sellado: por ejemplo, cuando ningún almacenamiento duradero guardó el documento.

prepare($request, $options = [])

POST /v1/dte/prepare. Exige issue; no abre el vault de firma, así que no manda la llave de firma. Reserva el correlativo y devuelve el documento canónico sin firmar, con su prepareToken. Úselo cuando necesita mostrar o revisar el documento antes de firmarlo. Devuelve un arreglo con estado "preparado", codigoGeneracion, numeroControl, tipoDte, ambiente, totales, documento y prepareToken.

documento queda como el árbol \stdClass que decodificó el cliente, a propósito: un objeto vacío {} dentro de él no debe volverse [] al regresar. Trátelo como opaco y páselo a sign() sin tocarlo. Si lo guarda entre las dos llamadas, use json_encode y json_decode($texto) (objetos), nunca json_decode($texto, true).

sign($prepared, $options = [])

POST /v1/dte/sign. Exige issue y manda la llave de firma. Firma y transmite lo que devolvió prepare(). Pase el resultado sin cambios: el servidor comprueba un MAC sobre el documento canónico, así que un centavo alterado se rechaza (prepare_token_invalid) en lugar de firmarse. Si le falta el prepareToken o el documento, lanza InvalidArgumentException antes de mandar nada. Devuelve lo mismo que issue().

Entrega por correo y WhatsApp

Emitir nunca espera a la entrega; son peticiones aparte. issue() solo marca canales con la opción deliver:

php
$resultado = $facta->issue($venta, [
    'idempotencyKey' => 'pedido-00417',
    'deliver' => [
        'email'    => true,   // o una dirección; true usa receptor.correo
        'whatsapp' => ['number' => '+50370001234', 'consent' => true],
    ],
]);
$token = $resultado['entrega']['token'];   // vale cinco minutos: es un secreto, guárdelo en su servidor

consent => true es la constancia de que el receptor aceptó recibir el documento por WhatsApp; el cliente exige que usted lo escriba. Una respuesta sellada trae entrega con un token válido cinco minutos y el estado inicial de cada canal. Un documento en contingencia no trae token.

deliverEmail($generationCode, $token, $options = [])

POST /v1/dte/{codigoGeneracion}/entrega/correo. Exige issue (o entrega:correo) y el token que devolvió la emisión. Responde el estado final del canal o en_proceso; una segunda llamada devuelve el estado vigente sin mandar un segundo mensaje. Un canal que no se puede entregar es un estado (fallido, sin_consentimiento…), no una excepción. Un token vencido es entrega_vencida, y uno equivocado, entrega_token_invalido; el cliente lo quita del mensaje y de details si el servidor lo repitió.

deliverWhatsApp($generationCode, $token, $options = [])

POST /v1/dte/{codigoGeneracion}/entrega/whatsapp. Exige issue (o entrega:whatsapp). Igual que el correo; se cobra del monedero prepagado de la empresa (sin_credito si no hay saldo).

getDelivery($generationCode, $options = [])

GET /v1/dte/{codigoGeneracion}/entrega. Exige query (o cualquiera de los dos alcances de entrega). Lee el estado de cada canal en canales; sigue funcionando cuando el token ya venció.

waitForDelivery($generationCode, $options = [])

Sondea getDelivery() hasta que cada canal esperado esté en un estado final (cualquiera menos pendiente o en_proceso). Opciones: channels (por defecto, los que reporta el servidor), timeoutMs (60000) e intervalMs (2000). Si se agota el plazo no lanza: devuelve la última lectura con settled => false, porque la entrega nunca cambia el resultado fiscal.

Consultar y listar

getDocumentStatus($generationCode, $options = [])

GET /v1/dte/{codigoGeneracion}. Exige query. Contesta también para documentos rechazados. Devuelve el estado (sellado, firmado, rechazado, contingencia, invalidado, reservado, liberado, descartado), numeroControl, tipoDte, ambiente, fecEmi, selloRecibido, totales y, si los hay, retornos y disponible. Un documento de otra empresa contesta not_found, igual que uno que no existe.

listDocuments($filters = [], $options = [])

GET /v1/dte. Exige query (y download si pide include). Una página a la vez. Filtros: desde, hasta, estado, tipoDte, limit, cursor e include => ['dte'], que agrega el Archivo DTE a cada fila. Devuelve documentos y siguiente; para la página que sigue pase siguiente como cursor, hasta que sea null. Un filtro desconocido lanza InvalidArgumentException. Con include, la página es de 25 como máximo; si se pasa, el servidor contesta include_limit_exceeded.

php
$cursor = null;
do {
    $pagina = $facta->listDocuments(['desde' => '2026-10-01', 'limit' => 100, 'cursor' => $cursor]);
    foreach ($pagina['documentos'] as $fila) { echo $fila['numeroControl'], "\n"; }
    $cursor = $pagina['siguiente'];
} while ($cursor !== null);

downloadDocument($generationCode, $kind = 'json', $options = [])

GET /v1/dte/{codigoGeneracion}/file. Exige download. Devuelve los bytes exactos que mandó el servidor, sin volver a serializarlos. kind es "json" (el Archivo DTE que recibe el receptor, por defecto), "pdf" o "ticket" (un tiquete de rollo dibujado en el servidor). El resultado es un arreglo con codigoGeneracion, kind, bytes, contentType y filename, y, cuando el servidor lo dice, storageSource (managed, holding o archive), jsonFormat (archivo-dte o raw) y paperWidthMm.

Opciones: raw => true (solo json: lo guardado tal cual, necesario para un documento en contingencia, que todavía no tiene Archivo DTE y de otro modo contesta not_sealed); source => 'managed' (solo json y pdf: exige la copia de Almacenamiento Facta, sin caer a la retención temporal); paperWidthMm (solo ticket, de 40 a 120, por defecto 80). Una combinación inválida lanza InvalidArgumentException antes de mandar nada.

php
$pdf = $facta->downloadDocument($codigo, 'pdf');
file_put_contents($codigo . '.pdf', $pdf['bytes']);   // escriba $pdf['bytes'] tal cual

El PDF lo dibuja el servidor con la plantilla de la empresa; este cliente no genera ninguno.

Anular y devolver

invalidate($generationCode, $request, $options = [])

POST /v1/dte/{codigoGeneracion}/invalidate. Exige issue y manda la llave de firma. Anula un documento sellado; es irreversible. $request lleva tipoAnulacion (1, 2 o 3), motivo, codigoGeneracionReemplazo cuando corresponde, y responsable y solicita, cada uno con nombre, tipoDocumento y numDocumento. Devuelve estado "invalidado" y el evento, o yaEstabaInvalidado => true. Si el documento tiene eventos de retorno, contesta has_return_events.

registerReturn($generationCode, $request, $options = [])

POST /v1/dte/{codigoGeneracion}/return. Exige issue y manda la llave de firma. Registra un evento de retorno sobre una factura, una exportación o un sujeto excluido que esta API emitió (guía). $request lleva items —cada una con su linea (contada desde 1) y exactamente una de cantidad o noGravado— y, opcional, fechaEvento (AAAA-MM-DD). El cliente comprueba esas reglas localmente y lanza InvalidArgumentException si el servidor las rechazaría de todas formas. Es irreversible una vez sellado.

Una respuesta 202 con estado "firmado" significa que Hacienda no contestó: repita con la misma idempotencyKey y el mismo cuerpo para reenviar el mismo evento firmado. Los rechazos son return_exceeds_available, return_window_closed y return_type_not_allowed.

Almacenamiento de Facta

Estos tres métodos hablan del almacenamiento administrado por Facta (JSON y PDF), no de sus destinos propios.

getStorageStatus($options = [])

GET /v1/storage/status. Exige download. Devuelve capabilityVersion, managed (cobertura, capacidad y si está listo), byos (si hay un destino propio verificado listo) y las listas supportedKinds y unsupportedKinds. No hace trabajo fiscal. Si el servidor no publica la ruta, lanza storage_unsupported; si la respuesta no cumple el contrato, storage_contract_invalid.

getDocumentCopies($options = [])

GET /v1/storage/copies. Exige download. Lista los recibos de las copias administradas del JSON y el PDF de esta empresa y este ambiente, incluidas las pendientes o fallidas, con state, bytes y sha256. Con la opción generationCode se limita a un documento. No devuelve el contenido del documento ni rutas de almacenamiento. Un recibo de otro ambiente o de otro documento se trata como storage_contract_invalid.

retryDocumentStorage($generationCode, $options = [])

POST /v1/storage/copies/{codigoGeneracion}/repair. Exige download e issue. Repara las copias de un documento que ya está sellado: no firma, no vuelve a emitir y no gasta un correlativo. Devuelve el recibo de almacenamiento (destination, environment, json, pdf, operationId). Si los bytes originales ya no existen, falla de forma explícita en lugar de inventarlos.

La emisión exitosa y la copia exitosa son cosas distintas. Si el recibo que viene dentro de una emisión no cumple el contrato, el cliente lo descarta y deja storageErrorCode: "storage_contract_invalid" en el resultado, conservando el documento sellado.

Retención y contrato

listHolding($limit = 50)

GET /v1/dte/holding. Exige download. Inspecciona el área de retención —los documentos que el servidor conserva un tiempo mientras no haya otra copia duradera—. No descarga bytes. Devuelve documentos.

getContract()

GET /v1/openapi.json. No exige alcance. El contrato OpenAPI que publica el servidor, como arreglo.

Catálogo de clientes y productos

El catálogo se lee y se escribe por la API, y depende de cómo lo guarda la empresa. Una llave con catalog:read lee un catálogo en claro o legible; escribir exige catalog:write y que el dueño haya permitido administrar el catálogo desde la API (lo que lo guarda sin cifrar en Facta). Los registros vuelven con los dos nombres de cada campo, el de la API y el almacenado (nombre y name, por ejemplo).

Con un catálogo cifrado, estas lecturas lanzan unauthorized (details.reason), porque leerlo necesitaría la instantánea local que esta versión no incluye.

MétodoQué hace
listCustomers($options)Lista los clientes activos; con includeInactive => true, también los desactivados. catalog:read.
getCustomer($customerId, $options)Un cliente por su id estable, o null. Un cliente desactivado también es null salvo includeInactive.
searchCustomers($query, $options)Busca por nombre, número de documento, NRC y correo. Opciones: limit (1 a 500, por defecto 50) e includeInactive.
listProducts($options)Lista los productos activos. catalog:read.
getProduct($productId, $options)Un producto por su id estable, o null.
searchProducts($query, $options)Busca en descripciones, códigos y códigos de barras.
createCustomer($input, $options)Crea un cliente. nombre es obligatorio. catalog:write. Con idempotencyKey, una creación reintentada es segura.
updateCustomer($customerId, $changes, $options)Cambia solo los campos que usted pasa. Para reactivar, ['activo' => true].
deactivateCustomer($customerId, $options)Desactiva (DELETE): deja de listarse y los documentos ya emitidos lo conservan. No existe el borrado definitivo.
createProduct($input, $options)Crea un producto.
updateProduct($productId, $changes, $options)Cambia solo los campos que pasa.
deactivateProduct($productId, $options)Desactiva un producto.

Un documento duplicado al crear un cliente, o un código duplicado en un producto, contesta catalog_duplicate; details.id dice cuál es y details.activo si está desactivado.

Idempotencia y reintentos

Todo método que puede gastar un correlativo necesita una Idempotency-Key estable. Pase la suya (idempotencyKey) y guárdela con el pedido: así la operación es idempotente aunque el proceso se reinicie, no solo entre los reintentos de una misma llamada. Si no la pasa, el cliente acuña una por llamada, una sola vez, fuera del bucle de reintentos, y la reenvía con cada intento. Generar una nueva por intento sería exactamente el error que la cabecera existe para evitar.

Lo mismo con el cuerpo: se codifica una vez por llamada y cada reintento reenvía esos mismos bytes.

La huella de la petición

issue() y prepare() mandan además X-Facta-Request-Fingerprint: el SHA-256 de la forma canónica (RFC 8785) de su petición tal como la escribió, antes de que el servidor resuelva ningún id del catálogo. Lo usa el servidor para reconocer un reintento aunque el catálogo haya cambiado entre el intento y su repetición; sin ella, esa repetición contestaría idempotency_key_reuse. Solo viaja en POST /v1/dte y POST /v1/dte/prepare; usted no tiene que calcularla.

Qué se reintenta

El cliente reintenta solo donde reintentar es seguro, hasta maxRetries veces más, con espera creciente (250 ms, 500 ms… hasta 4 s) o con el Retry-After del servidor cuando lo manda, y estos seis códigos son los únicos:

  • idempotency_in_flight: la primera petición con esa llave sigue trabajando.
  • mh_unreachable
  • service_unavailable
  • correlative_unavailable
  • storage_unavailable
  • network_error: la petición no obtuvo respuesta (fallo de conexión o plazo agotado).

Es seguro porque cada intento reenvía los mismos bytes con la misma Idempotency-Key. Nunca se reintenta un 4xx ni un rechazo de Hacienda. Cuando se agotan los intentos se lanza el último error. Los códigos que no están en esa lista son finales. Un 202 de contingencia no es un error: llega como resultado con estado "contingencia", y lo cuenta la guía Contingencia y resultados inciertos.

FactaError

Todo fallo es una Facta\Api\FactaError. Decida por el código, nunca por el mensaje: el texto es para quien lee el registro y puede reescribirse. Los códigos son los mismos que los del cliente de TypeScript.

MétodoQué devuelve
getErrorCode()Una de las constantes de FactaError (o un código que un servidor más nuevo añadió).
getStatus()El HTTP; 0 cuando la petición no obtuvo respuesta.
getDetails()Lo que el servidor mandó en details, ya sin secretos. validation_failed trae issues; rate_limited, la ventana y la espera.
getRetryAfterMs()Lo que pidió la cabecera Retry-After, en milisegundos, o null.
isRejection()true si Hacienda leyó el documento y lo negó (mh_rejected), frente a «no llegamos a Hacienda». Se arregla cambiando los datos, no esperando.
spent()['codigoGeneracion' => …, 'numeroControl' => …] si el fallo ya gastó un correlativo, o null. La corrección puede reusar ese número.
mhObservations()Lo que dijo Hacienda, palabra por palabra, como lista de textos.

El mensaje de un error ya no contiene ninguna de las llaves configuradas, y details omite los campos con aspecto de credencial.

php
try {
    $facta->issue($venta, ['idempotencyKey' => $pedido]);
} catch (FactaError $e) {
    switch ($e->getErrorCode()) {
        case FactaError::MH_REJECTED:
            error_log(json_encode(['gastado' => $e->spent(), 'observaciones' => $e->mhObservations()]));
            break;
        case FactaError::VALIDATION_FAILED:
            error_log(json_encode($e->getDetails()));
            break;
        default:
            throw $e;
    }
}

Además de FactaError, el cliente lanza InvalidArgumentException para un uso incorrecto que se detecta antes de mandar nada (una opción desconocida, un kind inválido, un retorno sin líneas) y Facta\Api\UnsupportedEnvironmentException si ese PHP no tiene ninguna forma de hacer peticiones HTTP.

Códigos de error

Los de abajo son los códigos que define el cliente; los del servidor tienen su explicación completa en la referencia HTTP. network_error, operation_outcome_unknown y archive_integrity_error son del propio cliente y nunca los manda el servidor; los dos últimos existen por compatibilidad con TypeScript y esta versión no los lanza.

CódigoQué significa y qué hacer
unauthorizedFalta la llave, o un catálogo cifrado no se puede leer en esta versión (mire details.reason).
invalid_api_keyLa llave no existe, está mal escrita o no empieza con facta_test_ o facta_live_.
key_revokedLa revocaron desde la app; no vuelve. Acuñe otra.
key_expiredPasó su fecha de vencimiento. Acuñe otra.
key_inactiveEstá desactivada. Reactívela desde la app.
forbidden_scopeLe falta el alcance que el método exige. details.required dice cuál.
dte_type_not_allowedLa llave no permite ese tipoDte. details.allowed trae la lista.
ip_not_allowedLa llave tiene lista de direcciones y la suya no está.
environment_not_allowedEl ambiente de la llave no es el de la empresa.
sign_key_requiredFalta la llave de firma factask_… en una operación que firma.
sign_key_invalidEsa contraseña no abre el vault de firma. details dice cuántos intentos quedan.
sign_vault_lockedDemasiados intentos fallidos: la llave se suspendió. Reactívela desde la app.
sign_vault_missingLa llave no está provisionada para firmar. Acuñe la llave de nuevo.
invalid_requestEl cuerpo o un campo tiene la forma equivocada. details.field señala el campo. No gasta correlativo.
include_limit_exceededinclude con más de 25 filas por página. Baje limit.
validation_failedEl documento no cumple el esquema oficial. details.issues lista cada campo. No gasta correlativo.
retention_mixed_class_unsupportedRetención de renta pedida sobre un documento con líneas exentas o no sujetas. Emítalas en otro documento.
not_foundNo existe el documento, el id o la ruta.
method_not_allowedLa ruta existe, pero no con ese método.
not_sealedSe pidió el Archivo DTE de un documento que todavía no tiene sello. Espere, o descargue con raw => true.
catalog_write_disabledEl dueño no ha permitido administrar el catálogo desde la API.
catalog_encryptedEl servidor no puede leer un catálogo cifrado.
catalog_duplicateYa existe ese cliente o producto. details.id dice cuál.
idempotency_key_requiredFalta la Idempotency-Key en una operación que no se puede deshacer.
idempotency_key_reuseEsa llave ya se usó con otra petición. Use una nueva, o repita exactamente la misma.
idempotency_in_flightLa primera petición con esa llave sigue en proceso. Se reintenta sola.
prepare_token_invalidEl prepareToken venció o el documento cambió. Vuelva a llamar a prepare().
return_exceeds_availableSe pidió devolver más de lo que queda. details.lineas dice cuánto.
return_window_closedEl evento está fuera del plazo de retorno.
return_type_not_allowedEse tipo de documento no admite un evento de retorno; se corrige con una nota de crédito.
return_pdf_unavailableYa no hay JSON firmado para dibujar la hoja del evento de retorno.
has_return_eventsNo se puede anular un documento que ya tiene eventos de retorno.
rate_limitedLa llave llegó a su techo por hora o por día. details dice cuánto esperar.
amount_limitEl documento pasa del monto máximo por documento de la llave. No gasta correlativo.
mh_rejectedHacienda leyó el documento y lo negó. Gastó correlativo; mire spent().
mh_unreachableReservado; se reintenta. Un fallo de transporte con Hacienda sale como contingencia.
correlative_unavailableNo se pudo reservar el correlativo. Se reintenta con la misma llave de idempotencia.
service_unavailableAlgo de Facta no contestó. Se reintenta con la misma llave.
no_storage_destinationUn destino propio reportado no está entre los publicados de la llave.
storage_unsupportedEl servidor todavía no publica esa ruta de almacenamiento administrado.
storage_unavailableEl almacenamiento administrado no contestó. Se reintenta; no vuelva a emitir.
storage_contract_invalidLa capacidad o el recibo contradice el contrato. Conserve el éxito fiscal y concilie el almacenamiento.
entrega_vencidaPasaron más de cinco minutos: el token ya no sirve. Lea el estado con getDelivery().
entrega_token_invalidoEl token no corresponde a este documento y canal.
canal_no_marcadoEl documento no marcó ese canal al emitirse.
internal_errorUn fallo imprevisto. Si se repite, escriba a soporte.
operation_outcome_unknownCódigo del cliente, definido para coincidir con el de TypeScript; esta versión no lo lanza todavía (llegará con la recuperación de operaciones).
archive_integrity_errorCódigo del cliente, definido para coincidir con el de TypeScript; esta versión no lo lanza, porque no incluye el archivo local cifrado.
network_errorLa petición no obtuvo respuesta. Se reintenta sola con la misma llave.

Lo que esta versión todavía no incluye

Esta es la primera etapa. Faltan las funciones que necesitan la llave de desbloqueo (factauk_…) y almacenamiento local, y no se han dejado a medias: simplemente no están.

  • La instantánea local del catálogo cifrado. Con un catálogo cifrado, el cliente de TypeScript descifra clientes y productos en su equipo; este no lo hace todavía. Mientras tanto sirven los catálogos legibles por la API o en claro, que el servidor lee por usted (customerId y productId se resuelven en el servidor), y siempre se puede mandar el receptor y las líneas completos.
  • Los destinos propios (BYOS) con la llave de desbloqueo. Replicar el JSON y el PDF a sus buckets, y avisar a Facta de esa copia, no está en esta versión.
  • El archivo local cifrado y la recuperación tras un reinicio. No hay equivalente a issueAndArchive() ni a recoverOperation(). Para sobrevivir a un reinicio use una idempotencyKey propia y guárdela con el pedido: repetir la llamada devuelve el mismo documento.
  • La impresión.

El camino normal no los necesita. Con Almacenamiento Facta conectado, el servidor guarda el JSON y el PDF de cada documento y getStorageStatus(), getDocumentCopies() y retryDocumentStorage() le dejan comprobarlo y repararlo. Ese es el camino que cubre esta versión.

Lo que este cliente no hace

  1. No calcula dinero. Ni IVA, ni retenciones, ni totales, ni el número de control, ni las fechas fiscales. Un cliente que calcule dinero sería un segundo motor fiscal, y dos motores se desincronizan el primer martes. Una prueba de arquitectura del paquete falla si aparecen en el código primitivas de firma o la construcción de campos fiscales.
  2. No firma. El certificado nunca pasa por aquí. Lo que sí viaja, y solo en las rutas que firman, es X-Facta-Sign-Key: la contraseña que abre el vault de firma en el servidor.
  3. No dibuja el PDF. El PDF y el ticket los dibuja el servidor con la plantilla de la empresa; downloadDocument() le entrega sus bytes exactos.
  4. No desactiva la verificación TLS. Ni siquiera con un servidor viejo: para eso existe caBundle.

Para el HTTP directo, sin esta librería, siga los ejemplos en PHP.

Escriba para buscar Por ejemplo: idempotencia, contingencia, 422, emitir.

↑ ↓ moverse Enter abrir Esc cerrar