@facta-dte/api — TypeScript SDK
The official TypeScript and JavaScript client for Facta's electronic tax document API. It uses the server as the fiscal authority: the SDK sends DTE data, handles credentials and retries, and can archive exact issued bytes for restart-safe recovery.
Read the Spanish README. The full method reference documents each public method's arguments, return value, required scope, side effects, errors, retries, and recovery behavior. The published API contract remains authoritative for HTTP fields and responses.
Package and source: npm @facta-dte/api · public GitHub repository.
Version selection: This source checkout is release 0.6.0, the stable version (npm latest). The npm registry is authoritative for published versions and distribution tags. Confirm that your installed published version includes a method before using it; validate source-only capabilities with a packed checkout.
Endpoint selection: FACTA_PRODUCTION_BASE_URL names the production endpoint used by default. Use baseUrl: FACTA_STAGING_BASE_URL to opt into staging, or provide a custom endpoint. An official PHP SDK is also available at facta-dte/api.
Official SDKs are available for TypeScript/JavaScript and PHP. Direct HTTP examples do not represent published SDKs.
License: MIT. See LICENSE.
Runtime support
The SDK root entry works in Node.js 22+, Deno, and Bun and does not eagerly import filesystem modules. The @facta-dte/api/node entry adds Node-only encrypted file archives and config-file loading. The dedicated @facta-dte/api/file-archive entry is also Node-only.
The portable client relies on standard fetch, Web Crypto, AbortSignal, and crypto.randomUUID. Every request uses the configured API endpoint; a facta_test_ key selects Hacienda's test environment at that same endpoint. The client defaults to the production URL; to test on staging pass baseUrl: FACTA_STAGING_BASE_URL (https://eobxzotnqzgtpuqvmpkc.supabase.co/functions/v1/api-v1) with a facta_test_ key from the staging app.
Quick start
# Verify published versions and tags, then install the approved stable release:
npm view @facta-dte/api version dist-tags
pnpm add @facta-dte/api
# Optionally install a prerelease, when one is published:
pnpm add @facta-dte/api@next
import { Facta, type DteRequest } from "@facta-dte/api";
const facta = new Facta({
apiKey: process.env.FACTA_API_KEY!,
signKey: process.env.FACTA_SIGN_KEY!,
});
const sale: DteRequest = {
tipoDte: "01",
items: [{ descripcion: "Sample item", cantidad: 1, precioUni: 10 }],
};
const result = await facta.issue(sale, { idempotencyKey: "order-1042" });
tipoDte, codigoGeneracion, and all other fiscal wire fields keep the names and values defined by the Facta API. Public TypeScript identifiers are English; fiscal payload properties are not translated.
An IssueResult is a discriminated union. Check result.estado: "sellado" means Hacienda returned a seal; "contingencia" means Facta accepted and signed the DTE but needs reconciliation. Do not submit the sale with a new idempotency key after an uncertain response.
Credentials and configuration
| Option | Purpose |
|---|---|
apiKey | Required facta_test_… or facta_live_… key. The key selects the Hacienda environment. |
signKey | Required for issue, prepare + sign, invalidate, and registerReturn. Sent only on signing operations. |
unlockKey | Optional factauk_… key used locally to decrypt the synchronized destination bundle. Never sent to the API. |
baseUrl | Optional API host. Defaults to Facta's public API URL. |
timeoutMs | Request timeout, default 60,000 ms, including response-body reading. |
region | Functions region sent as x-region so requests run next to the database. Default: read once from /v1/status (fallback us-west-2 for older APIs). A string overrides, false disables; also config.region and the FACTA_API_REGION environment variable. await facta.region() returns it; diagnose() reports the served region. |
maxRetries | Retry limit for explicitly retryable transport/service failures; defaults to 3. |
fetch | Optional fetch implementation for an owning runtime or tests. |
clock | Reference clock for archive timestamps and S3 signing: true (default, the public https://clock.factadte.com/), a URL, or false for the device clock. Calibrated lazily and never fails an operation. Archive methods (issueAndArchive, recoverOperation, invalidateAndArchive, replicateArchive) wait for the first calibration of a process: well under a second normally, about 6 s (two 3 s timeouts) when the service is unreachable; later calls answer locally. After a failure the SDK falls back to the device clock. |
clockFetch | Optional fetch used only to calibrate the clock. |
config | Optional versioned scalar configuration (FactaConfigV1). |
runtime | Optional default archive, invalidation archive, remote destinations, and print transport (FactaRuntimeConfigV1). |
Configuration values do not include credentials. Flat legacy options override matching versioned config values. See the Node guide or Deno guide for runtime setup and environment permissions.
Reference clock
A document's own date and time are set by Facta's server, never by this SDK. The SDK's clock matters for two local things: SigV4 signatures on S3-compatible uploads (S3 refuses a request signed more than 15 minutes from its own time, RequestTimeTooSkewed) and the timestamps in archive records. new Facta({ apiKey }) keeps one NTP-style reference clock (facta.clock): three calibration samples at first use, then the monotonic clock answers locally and recalibrates only when its uncertainty passes 500 ms, after nextSyncAfterMs or when the device time jumps. Share it with an S3 destination with createS3ArtifactDestination({ ..., clock: facta.clock ?? false }); without it the destination keeps its own. An unreachable clock service falls back to the device clock. See the clock guide.
Public client surface
The Facta client exposes the following English methods:
- Status and diagnostics:
status,diagnose,getContract,getStorageStatus. - Destinations and catalog reads:
syncDestinations,listCustomers,getCustomer,searchCustomers,listProducts,getProduct,searchProducts. - Catalog writes (plain-text catalog, scope
catalog:write):createCustomer,updateCustomer,deactivateCustomer,createProduct,updateProduct,deactivateProduct. Reads go through the API in every catalog mode; the server resolvescustomerId/productId. See the catalog write guide. - Catalogue changes and webhooks (scope
catalog:read):listCatalogChanges,createWebhook,listWebhooks,getWebhook,updateWebhook,deleteWebhook,rotateWebhookSecret,testWebhook, and the receiver-sideverifyWebhook/constructWebhookEvent/signWebhook. Records read one at a time carryrevision; pass it asifMatchtoupdateCustomer/updateProduct. See the webhooks guide. - Drafts (scope
draft):createDraft,listDrafts,getDraft,getDraftByKey,updateDraft(versionsent asIf-Match),cancelDraftandissueDraft: leave an invoice for a person of the company to review and issue. A misspelled field comes back indraft.unknownFields(it is not in the draft). See the drafts guide. - DTE lifecycle:
issue,prepare,sign,getDocumentStatus,listDocuments,invalidate,registerReturn,listHolding,downloadDocument,getDocumentCopies,retryDocumentStorage. - Temporary copy and confirmation (new):
listUnconfirmedDocuments,confirmDocumentStorage,storeAndConfirmUnconfirmed. Facta deletes its temporary encrypted copy 2 days after you report a document stored, or 30 days after sealing if nobody does; the company owner is mailed from day one. See the temporary copy. - Durable archival:
issueAndArchive,recoverOperation,listPendingOperations,invalidateAndArchive,recoverInvalidation,listPendingInvalidations,replicateArchive,diagnoseDestinations. - Delivery by e-mail and WhatsApp:
issue(request, { deliver }),deliverEmail,deliverWhatsApp,getDelivery,waitForDelivery. Issuing never waits for delivery; each channel is a separate request with a five-minute token and failures are reported as states. - Local printing:
print.
The full method reference describes every method's inputs, return type, credential scope, network/local effects, errors, and retry or recovery behavior.
Safe issuance and recovery
Every operation that can reserve a fiscal control number needs a stable idempotencyKey. Reuse the same key with the same request after a timeout; a changed request with that key is rejected. AbortSignal stops local waiting/retries but cannot prove that a request already accepted by the server was cancelled.
For applications that must survive a process restart, use issueAndArchive with a durable archive, operationId, and idempotencyKey:
import { Facta, type DteRequest } from "@facta-dte/api";
import { FileInvoiceArchive } from "@facta-dte/api/node";
const archive = await FileInvoiceArchive.open({
directory: "./private-invoices",
passphrase: process.env.FACTA_ARCHIVE_PASSPHRASE!,
});
const facta = new Facta({
apiKey: process.env.FACTA_API_KEY!,
signKey: process.env.FACTA_SIGN_KEY!,
runtime: { archive },
});
const sale: DteRequest = {
tipoDte: "01",
items: [{ descripcion: "Sample item", cantidad: 1, precioUni: 10 }],
};
const result = await facta.issueAndArchive(sale, {
includeTicket: false,
operationId: "order-1042",
idempotencyKey: "order-1042",
});
if (result.archive.state !== "complete") {
// Fiscal success and archival status are separate. Keep the operation ID.
await facta.recoverOperation("order-1042");
}
issueAndArchive stores the immutable resolved request and operation identity before the fiscal request. It archives the exact signed JSON and PDF returned by the server, derives the JWS from that JSON, and optionally requests a receipt ticket PDF only from APIs that support it. Use includeTicket: false to archive JSON, PDF and JWS without requesting a ticket. Ticket archival is enabled by default and requires a deployed API with kind=ticket PDF support. Recovery verifies endpoint, API key identity, issuer, and environment before any fiscal request. Legacy journals without that identity remain inspectable but cannot be replayed automatically.
An archive is sensitive data. FileInvoiceArchive uses authenticated encryption and restrictive local file permissions; its passphrase must be independent from API, signing, and vault credentials. Keep backups and retention under the application's control. Read storage adapters for remote-copy adapters and reconciliation.
Separate preparation and signing
Use prepare and sign only when a human or approval system must review the canonical document before signing:
const prepared = await facta.prepare(sale, { idempotencyKey: "order-1042:prepare" });
// Review prepared.documento and prepared.totales without changing them.
const result = await facta.sign(prepared, { idempotencyKey: "order-1042:sign" });
Preparation reserves a control number. The prepared document and token must be passed unchanged to sign; an edited document is rejected. prepare does not use signKey, while sign does. Both routes require an idempotency key, and the API scopes keys by API key, not by route: use a different key for each step (never reuse the prepare key on sign, which the API refuses with idempotency_key_reuse). See the prepare/sign guide.
DTE types and fiscal payloads
The exported TypeScript types include DteRequest, IssueResult, PreparedDte, DocumentStatus, InvalidationRequest, InvalidationResult, DteType, Recipient, Address, LineItem, Totals, and DtePage. DTE request types form a union keyed by the unchanged tipoDte field, covering "01", "03", "05", "06", "11", and "14".
The type system helps construct payloads but does not replace server-side tax validation. The SDK has no local VAT engine and does not infer fiscal totals. For a catalog-backed price, the item's VAT basis must match the DTE type; provide an explicit precioUni in the document's expected basis to override the catalog price.
Catalog and destination snapshots
syncDestinations() decrypts the published destinations snapshot in memory using unlockKey. It does not create or update server records and does not write artifacts to storage destinations. Since 0.6 there is no catalog snapshot: customerId / productId are resolved by the server, and the catalog reads call the API.
issue() never writes to storage. With unlockKey and a published snapshot, issueAndArchive() and recoverOperation() also replicate the legal JSON and PDF to your synced destinations under Facta's canonical DTE/…/YYYY/MM/<numeroControl> paths (opt out with replicate: false), report each copy they wrote to Facta (a report, not a verification), and surface problems as non-throwing warnings. See what each mode does.
- Catalog reads and id resolution
- Create, edit and deactivate customers and products
- Storage adapters
- The temporary copy, the confirmation and the emergency store: the one-hour holding, the 2-day / 30-day temporary copy, the owner mails, and how to report a document stored
- Emergency safeguard:
runtime.emergencyStoresaves the JSON and PDF when Facta could not store them durably (it does not confirm; reporting does) - Node integration
- Deno integration
The SDK sends nothing itself: e-mail and WhatsApp delivery is performed by the API after you mark the channels. It does not sign locally, choose a signing certificate, or replace the API's fiscal contract.
The Archivo DTE (what the receiver gets)
A sealed result carries archivoDte: the exact UTF-8 text of the Archivo DTE, which is the signed document plus firmaElectronica (the JWS, byte-for-byte) and selloRecibido. This is the file to give a customer or an accountant. It is absent in contingency, where there is no seal yet. archivoJson is unchanged (the stored original, {codigoGeneracion, ambiente, jws}); keep archiving that.
import { archivoDteOf } from "@facta-dte/api";
const result = await facta.issue(sale);
if (result.estado === "sellado") {
// `archivoDte` when the API serves it; otherwise built from documento + jws + selloRecibido.
const archivo = archivoDteOf(result);
}
const file = await facta.downloadDocument(code, "json"); // Archivo DTE (default)
const original = await facta.downloadDocument(code, "json", { raw: true }); // stored bytes
console.log(file.jsonFormat, original.jsonFormat); // "archivo-dte" "raw"
downloadDocument(code, "json") now returns the Archivo DTE by default and reports jsonFormat from the X-Facta-Json-Format header; { raw: true } returns the stored original and is valid only for json. A document without a seal answers 409 not_sealed; use raw: true for its original. The React receipt and download button give the Archivo DTE as «Descargar JSON»; «JSON original (raw)» appears only with the rawJson prop, and the server handler honours it only when capabilities.rawJson is true. Local copies and replication still store exactly what they stored before.
Errors
Authenticated API failures use FactaError, with a stable code, HTTP status, safe details, isRejection, and optional spent control-number information. Branch on code, not localized message text. Local validation may throw TypeError or RangeError; archive and storage adapters can throw their own typed errors. Credential-like values are redacted from API error details.
Retry only the same request with the same idempotency key. operation_outcome_unknown and archive integrity failures require inspection or reconciliation; they do not authorize issuing a replacement DTE. The method reference includes outcomes and recovery guidance for each public operation, and the error catalogue lists every error code with its HTTP status, retry behaviour and what to do.
Examples and checks
Typed examples live in examples/. The package's test suite uses Deno. pack:check builds the Node ESM bundles and declarations, packs the package in a temporary directory, then checks fresh Node and Deno consumers against the packed output.
pnpm test
pnpm pack:check
Agent skill for AI coding agents
skills/facta-dte-api/ is an installable agent skill (SKILL.md + references + type-checked starter templates) that teaches a coding agent how to integrate this SDK and the Facta DTE HTTP API correctly: keys, DTE types, idempotency, contingency, files and delivery, errors and the production checklist. Install and download instructions (English and Spanish): skills/README.md. pnpm skill:pack builds dist/facta-dte-api-skill.zip.
Guides
Every guide in guides/, in English and Spanish. The two React guides are English only.
| Topic | English | Español |
|---|---|---|
| Method reference: every public method | reference.md | reference.es.md |
| Node.js integration | node.md | node.es.md |
| Deno integration | deno.md | deno.es.md |
| Error catalogue: every code, HTTP status, retries | errors.md | errors.es.md |
| Idempotency patterns and recovering uncertain results | idempotency.md | idempotency.es.md |
| Pending documents: list and retry without issuing twice | pending-documents.md | pending-documents.es.md |
| Prepare, review, then sign | prepare-sign.md | prepare-sign.es.md |
| The Archivo DTE (what the receiver gets) | archivo-dte.md | archivo-dte.es.md |
| Delivery by e-mail and WhatsApp | delivery.md | delivery.es.md |
Payment, global discount (descuentoGlobal), non-taxed lines (noGravado), exports, income-tax withholding | payments-export-renta.md | payments-export-renta.es.md |
| Returns (Evento de Retorno) | return-event.md | return-event.es.md |
Diagnostics, diagnoseDestinations | diagnose.md | diagnose.es.md |
| Regional pinning | region.md | region.es.md |
| Debug timings | timings.md | timings.es.md |
| Reference clock | reference-clock.md | reference-clock.es.md |
| Catalog reads and id resolution | catalog.md | catalog.es.md |
| Catalog writes (customers and products) | catalog-write.md | catalog-write.es.md |
| Catalogue changes and webhooks | webhooks.md | webhooks.es.md |
| Drafts | drafts.md | drafts.es.md |
| Storage adapters | storage-adapters.md | storage-adapters.es.md |
| The temporary copy and the confirmation | temporary-copy.md | temporary-copy.es.md |
| Emergency safeguard | emergency.md | emergency.es.md |
Headless browser client (@facta-dte/api/browser) | browser.md | browser.es.md |
| Signing window for React | react.md | — |
| Signing window: server side and storage | react-server.md | — |
Documentation language
The repository keeps the Spanish documentation at README.es.md, alongside the English guides in the method reference and guides. Each guide links to its Spanish counterpart.
Public live-test reports
The staging live workflow publishes only a validation table in the PR and job summary. It verifies the issued test invoice, signed JSON and PDF in memory and an encrypted temporary archive, then deletes the archive. It never uploads invoice documents or publishes issuer/customer identifiers, generation codes, control numbers, raw API errors or assertion payloads.
Document query and listing receiver metadata can be null when a document was encrypted in the Facta app, imported or restored with a company key unavailable to the API. The SDK preserves null; a current catalog customer does not establish the historical receiver. unlockKey opens only the published catalog/destination snapshots, not historical invoice receiver blobs.
The full staging live workflow requires, for BYOS setups, its owner-published destination snapshot to open before issuing its test invoice. Ready managed-only setups do not require a BYOS snapshot. An unavailable required snapshot fails the run; library issuance compatibility with legacy status responses does not waive this integration gate.
Executable workflow examples and storage failures
The shipped workflow examples cover managed emission, managed plus encrypted local/BYOS copies, restart recovery, catalog references, 58 mm tickets, designated test invalidations and older-server diagnostics. The six DTE request examples are typed fixture templates: replace recipient placeholders and supply appropriate issuer-owned sealed references for notes before live use. Imports perform no fiscal work.
Install @facta-dte/api, copy the examples into your project and call the
exported functions explicitly. For the minimal example:
node --experimental-strip-types --input-type=module -e "import { main } from './examples/hola-factura.ts'; await main();"
deno eval "import { main } from './examples/hola-factura.ts'; await main();"
Set a test API key, explicit API URL and stable ERP_ORDER_ID first. Missing
configuration stops the minimal example before issuance. pack:check compiles
all shipped examples and executes their functions against synthetic fixtures
in Node/Deno using the packed package. This proves example compatibility;
live fiscal prerequisites remain separate validation gates.
A malformed storage receipt does not undo fiscal success: storageErrorCode
is storage_contract_invalid, and the invalid receipt is omitted. The file
archive persists that condition and includes it in pending operations until
repair supplies a valid receipt. Never retry with a new fiscal identity.
Present malformed storage capability blocks diagnostics; absent older routes
remain explicitly unknown. Managed-only readiness makes BYOS synchronization
optional while signing and requested catalog references retain their checks.
The staging live suite requires managed readiness, verifies durable JSON/PDF receipts against exact inline bytes, reads them back from managed storage, replays the original idempotency key, repairs only that invoice and checks that storage accounting stays unchanged. It publishes fixed result labels only. Ticket/invalidation and six fiscal variants still need explicitly designated fixtures and their fiscal prerequisites before a live support claim.
Use downloadDocument(generationCode, "pdf", { source: "managed" }) to require a managed JSON/PDF copy without holding fallback. Ticket requests reject this option locally; an older server without source proof returns storage_unsupported. The default download order is unchanged.
Release automation
Maintainers publish from the publish.yml GitHub Actions workflow in
Facta-DTE/facta-api-sdk using npm Trusted Publishing (OIDC), without a saved
npm publishing token. The trusted publisher must permit direct npm publish.
A v<package-version> tag triggers tests, packed-consumer verification and
publication. The tagged commit must belong to main and match package.json.
Stable versions use latest; prereleases use their prerelease tag.
Merge and validate a release on main, then push a new version tag. Never move
an existing release tag or try to republish a published version. The workflow
publishes directly; there is no separate npm staged-package approval step.