@facta-dte/api

中文文档
0.6.0发布于 2026/11/15MITTypeScriptES Module零依赖构建来源已验证近 30 天下载 4408

TypeScript client for the Facta electronic invoicing API (DTE) in El Salvador.

原始使用文档
文档目录18 节
  1. Runtime support
  2. Quick start
  3. Credentials and configuration
  4. Reference clock
  5. Public client surface
  6. Safe issuance and recovery
  7. Separate preparation and signing
  8. DTE types and fiscal payloads
  9. Catalog and destination snapshots
  10. The Archivo DTE (what the receiver gets)
  11. Errors
  12. Examples and checks
  13. Agent skill for AI coding agents
  14. Guides
  15. Documentation language
  16. Public live-test reports
  17. Executable workflow examples and storage failures
  18. Release automation

@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

OptionPurpose
apiKeyRequired facta_test_… or facta_live_… key. The key selects the Hacienda environment.
signKeyRequired for issue, prepare + sign, invalidate, and registerReturn. Sent only on signing operations.
unlockKeyOptional factauk_… key used locally to decrypt the synchronized destination bundle. Never sent to the API.
baseUrlOptional API host. Defaults to Facta's public API URL.
timeoutMsRequest timeout, default 60,000 ms, including response-body reading.
regionFunctions 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.
maxRetriesRetry limit for explicitly retryable transport/service failures; defaults to 3.
fetchOptional fetch implementation for an owning runtime or tests.
clockReference 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.
clockFetchOptional fetch used only to calibrate the clock.
configOptional versioned scalar configuration (FactaConfigV1).
runtimeOptional 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 resolves customerId / productId. See the catalog write guide.
  • Catalogue changes and webhooks (scope catalog:read): listCatalogChanges, createWebhook, listWebhooks, getWebhook, updateWebhook, deleteWebhook, rotateWebhookSecret, testWebhook, and the receiver-side verifyWebhook / constructWebhookEvent / signWebhook. Records read one at a time carry revision; pass it as ifMatch to updateCustomer / updateProduct. See the webhooks guide.
  • Drafts (scope draft): createDraft, listDrafts, getDraft, getDraftByKey, updateDraft (version sent as If-Match), cancelDraft and issueDraft: leave an invoice for a person of the company to review and issue. A misspelled field comes back in draft.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.

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.

TopicEnglishEspañol
Method reference: every public methodreference.mdreference.es.md
Node.js integrationnode.mdnode.es.md
Deno integrationdeno.mddeno.es.md
Error catalogue: every code, HTTP status, retrieserrors.mderrors.es.md
Idempotency patterns and recovering uncertain resultsidempotency.mdidempotency.es.md
Pending documents: list and retry without issuing twicepending-documents.mdpending-documents.es.md
Prepare, review, then signprepare-sign.mdprepare-sign.es.md
The Archivo DTE (what the receiver gets)archivo-dte.mdarchivo-dte.es.md
Delivery by e-mail and WhatsAppdelivery.mddelivery.es.md
Payment, global discount (descuentoGlobal), non-taxed lines (noGravado), exports, income-tax withholdingpayments-export-renta.mdpayments-export-renta.es.md
Returns (Evento de Retorno)return-event.mdreturn-event.es.md
Diagnostics, diagnoseDestinationsdiagnose.mddiagnose.es.md
Regional pinningregion.mdregion.es.md
Debug timingstimings.mdtimings.es.md
Reference clockreference-clock.mdreference-clock.es.md
Catalog reads and id resolutioncatalog.mdcatalog.es.md
Catalog writes (customers and products)catalog-write.mdcatalog-write.es.md
Catalogue changes and webhookswebhooks.mdwebhooks.es.md
Draftsdrafts.mddrafts.es.md
Storage adaptersstorage-adapters.mdstorage-adapters.es.md
The temporary copy and the confirmationtemporary-copy.mdtemporary-copy.es.md
Emergency safeguardemergency.mdemergency.es.md
Headless browser client (@facta-dte/api/browser)browser.mdbrowser.es.md
Signing window for Reactreact.md—
Signing window: server side and storagereact-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.

版本与发布记录

@facta-dte/api 共发布 16 个版本,首次发布于 2026/10/04;过去 12 个月发布了 16 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.6.0latest2026/11/15今天
0.6.0-next.4next2026/11/0214 天前
0.6.0-next.32026/10/3017 天前
0.6.0-next.22026/10/2917 天前
0.6.0-next.12026/10/2918 天前
0.5.32026/10/2126 天前

其他发布通道:beta → 0.1.0-beta.1,next → 0.6.0-next.4。

关于 @facta-dte/api 的常见问题

@facta-dte/api 是什么?

TypeScript client for the Facta electronic invoicing API (DTE) in El Salvador.

如何安装 @facta-dte/api?

在项目目录运行 npm install @facta-dte/api,或使用 pnpm add / yarn add / bun add。

@facta-dte/api 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@facta-dte/api 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@facta-dte/api 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@facta-dte/api 可以商用吗?

@facta-dte/api 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@facta-dte/api 的下载量有多少?

根据 npm 官方下载统计,@facta-dte/api 在 2026/10/14 – 2026/11/12 这 30 天内被下载 4,408 次,日均约 147 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@facta-dte/api 还在维护吗?

最新版本 0.6.0 发布于 2026/11/15(今天);至今共发布 16 个版本,其中过去 12 个月发布了 16 个;首次发布于 2026/10/04。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。