@devgoliiive/unidigital-sdk

中文文档
0.1.4发布于 2026/10/13UNLICENSEDTypeScriptESM + CJS零依赖近 30 天下载 2821

Typed client for the Unidigital fiscal billing API, with webhook signature verification.

原始使用文档
文档目录17 节
  1. Instalación
  2. Skill para agentes
  3. Uso
  4. Construir el sdk
  5. Chequeo de conectividad
  6. Resolver una serie
  7. Emitir una factura
  8. Emitir una nota de crédito o débito
  9. Consultar el estado
  10. Obtener el enlace del documento
  11. Anular un documento
  12. Verificar un webhook
  13. Errores
  14. Si compilas sin strict
  15. Reintentos
  16. Dobles de test
  17. Versionado y publicación

@devgoliiive/unidigital-sdk

El sdk tipado del servicio de facturación fiscal Unidigital. Envuelve las operaciones de /v1, verifica las firmas de los webhooks y normaliza los errores en una jerarquía discriminada por un código estable.

El sdk hace tres cosas y ninguna más. No reintenta respuestas HTTP, no espera a que un documento se finalice, no valida el payload en local y no trae dependencias en runtime.

Instalación

El sdk se publica como paquete público en npmjs.com, bajo la organización devgoliiive. No hace falta .npmrc, bunfig.toml ni token: ni en local, ni en CI, ni en el build de Railway.

pnpm add @devgoliiive/unidigital-sdk
# o
bun add @devgoliiive/unidigital-sdk

Skill para agentes

El paquete trae una skill de Claude Code con las invariantes del SDK, versionada con él. Enlázala desde la raíz de tu repo:

mkdir -p .claude/skills
ln -s ../../node_modules/@devgoliiive/unidigital-sdk/skills/unidigital-sdk .claude/skills/unidigital-sdk

Uso

Construir el sdk

import { createClient } from "@devgoliiive/unidigital-sdk";

const unidigital = createClient({
  baseUrl: process.env.UNIDIGITAL_API_URL!,
  apiKey: process.env.UNIDIGITAL_API_KEY!,
  // Opcionales:
  timeoutMs: 10_000, // por defecto; ajustable por llamada
  retry: { attempts: 2 }, // solo fallos de transporte, nunca respuestas HTTP
  // fetch: miFetchInstrumentado,
});

No hay URL por defecto: hornear una envejece mal, y apuntar al entorno equivocado emite documentos fiscales reales.

Chequeo de conectividad

Verifica la conectividad y las credenciales antes de emitir nada real.

await unidigital.checkConnectivity();

Resolver una serie

const { series } = await unidigital.series.lookup({
  unidigitalAccountId: accountId,
});
const seriesId = series.find((serie) => serie.name === "A")?.seriesId;

Cada serie trae el seriesId que pide documents.issue. El lookup registra las series que Unidigital reporta, así que una serie nueva aparece en la siguiente llamada. Si tu catálogo no encuentra una serie, vuelve a consultarlo. Una serie con seriesId: null tiene su nombre ocupado por otra serie de la misma cuenta. No puedes emitir con ella hasta que un operador lo corrija en el admin.

Emitir una factura

const { trackingId, status } = await unidigital.documents.issue({
  unidigitalAccountId: accountId,
  seriesId,
  idempotencyKey: order.id, // obligatoria, y sale de tu dominio
  payload: {
    Name: "Carlos Perez",
    FiscalRegistryCode: "J",
    FiscalRegistry: "297182748",
    Address: "Av. Principal, Caracas",
    PaymentType: "CONTADO",
    EmailTo: "[email protected]",
    EmissionDateAndTime: "2026-01-02T10:00:00Z",
    Currency: "USD",
    TaxBase: 100,
    TaxAmount: 16,
    Total: 116,
    GrandTotal: 116,
    IGTFPercentage: 3,
    TaxPercentReduced: 8,
    TaxPercentSumptuary: 31,
    Details: [
      {
        Description: "Boleto general",
        Quantity: 1,
        UnitPrice: 100,
        Amount: 100,
        TaxAmount: 16,
        TaxPercent: 16,
        TotalAmount: 116,
        OperationCode: "C001",
        ProductType: 1,
      },
    ],
  },
});

La clave de idempotencia es obligatoria y el SDK nunca la genera. Un reintento tiene que reutilizar la misma clave. Si generas una nueva, el servicio emite dos documentos fiscales con dos correlativos quemados.

Los campos que el servicio se asigna a sí mismo —Number, SerieStrongId, DocumentType y los Affected* que describen el documento afectado— no son enviables: el tipo los rechaza antes del round trip.

Emitir una nota de crédito o débito

El documento afectado va por su id, y nada más sobre él.

await unidigital.notes.issue({
  unidigitalAccountId: accountId,
  seriesId,
  documentType: "credit_note", // o "debit_note"
  affectedDocumentId: invoiceId,
  idempotencyKey: refund.id,
  payload: {/* como arriba */},
});

Consultar el estado

Es el respaldo cuando el webhook no llegó. Las lecturas (getStatus, getContent, void) aceptan el id del documento o el trackingId que devolvió issue.

const doc = await unidigital.documents.getStatus(trackingId);

doc.invoiceNumber; // el correlativo dentro de la serie
doc.controlNumber; // el número de control del SENIAT, null hasta `issued`

El correlativo y el número de control son números distintos. Un documento no está legalmente completo hasta tener el segundo.

No hay helper de espera a estado final, y es deliberado: la finalización tarda minutos, así que una espera bloqueante dentro de un handler HTTP es una caída esperando su turno. Reconcilia desde un job con esta lectura.

Obtener el enlace del documento

const { url } = await unidigital.documents.getContent(documentId);

Anular un documento

const result = await unidigital.documents.void(documentId);

if (result.outcome === "voided_local_record_pending") {
  // La anulación SÍ ocurrió en el proveedor; solo nuestro registro no ha
  // cuadrado, y el servicio lo cierra con un barrido propio. No es un fallo.
}

Es síncrona, no lleva clave de idempotencia (anular dos veces es seguro) y el SDK no la reintenta.

Verificar un webhook

Es una función suelta: verificar no necesita API key ni URL base, y el proceso que recibe webhooks rara vez es el que emite documentos.

import { verifyWebhook, WebhookVerificationError } from "@devgoliiive/unidigital-sdk";

// El cuerpo tiene que ser el CRUDO, tal cual llegó. Re-serializarlo reordena
// claves y la firma deja de coincidir.
try {
  const event = verifyWebhook({
    rawBody,
    headers: req.headers,
    secret: process.env.UNIDIGITAL_WEBHOOK_SECRET!,
    toleranceSeconds: 300, // por defecto
  });

  // Deduplica por el delivery id, NUNCA por el document id: un mismo documento
  // genera más de un evento, y cada uno es un hecho distinto.
  if (await alreadyProcessed(event.deliveryId)) return;

  switch (event.type) {
    case "fiscal_document.issued":
      // event.data.controlNumber ya está disponible
      break;
    case "fiscal_document.approved":
    case "fiscal_document.rejected":
    case "fiscal_document.voided":
      break;
  }
} catch (error) {
  if (error instanceof WebhookVerificationError) {
    // error.reason: "missing_header" | "malformed_header" | "invalid_secret"
    //             | "timestamp_out_of_window" | "signature_mismatch"
    //             | "malformed_body"
  }
}

Errores

Toda respuesta no exitosa llega como excepción tipada. Discrimina por code, nunca por el mensaje: el mensaje es libre, lleva el texto de Unidigital en un fallo aguas arriba, y cambia cuando alguien corrige una errata.

Hay tres raíces de error, y un catch completo las cubre las tres: UnidigitalError (el servicio respondió), UnexpectedResponseError (respondió algo que el envelope no explica) y TransportError (nunca hubo respuesta). El guard isUnidigitalSdkError cubre las tres de una vez.

import { ConflictError, UnidigitalError, ValidationError } from "@devgoliiive/unidigital-sdk";

try {
  await unidigital.documents.void(documentId);
} catch (error) {
  if (error instanceof ConflictError) {
    if (error.code === "void_in_flight") {
      // Ya hay una anulación en vuelo: espera y consulta el estado.
    }
    if (error.code === "document_not_voidable") {
      // Definitivo: el documento no existe fiscalmente. Ríndete.
    }
  }

  if (error instanceof ValidationError) {
    error.issues; // qué campo corregir
  }

  if (error instanceof UnidigitalError) {
    error.code;
    error.status;
    error.requestId; // cítalo al pedir soporte
  }
}
ClaseStatusPara qué sirve
ValidationError400El payload es inválido; issues nombra el campo.
AuthenticationError401API key ausente, inválida o sin tenant.
PermissionError403El tenant no tiene permiso sobre esa cuenta.
NotFoundError404No existe — o es de otro tenant, que responde igual.
ConflictError409El estado del documento impide la operación.
UpstreamError502Fallo de Unidigital.
ServiceUnavailableError503Permanente: puerto sin cablear o credenciales.
InternalError500Error nuestro. Cita el requestId.
UnexpectedResponseError—La respuesta no encaja en el envelope; conserva status y cuerpo.
TransportError—Fallo de transporte agotado. No es una respuesta del servicio.

Si compilas sin strict

El compilador no te mostrará ningún | null de esta superficie. Los campos que pueden faltar son: en DocumentStatusView, strongId, invoiceNumber, controlNumber, batchStrongId, rejectionReason e issuedAt; en el data de un webhook, invoiceNumber, controlNumber, issuedAt y reason; y issues en ValidationError. Compruébalos a mano.

Reintentos

El SDK reintenta solo fallos de transporte (DNS, conexión reiniciada, socket colgado, timeout), con tope bajo y backoff con jitter, reusando la misma clave de idempotencia.

Ninguna respuesta HTTP se reintenta, aunque la intuición diga lo contrario: aquí un 503 es una condición permanente, y un 502 en la anulación puede significar que la anulación sí se aplicó, caso que el servicio reconcilia con un barrido propio.

Dobles de test

La interfaz del sdk se exporta como tipo, para que escribas tus propios dobles y se rompan en CI cuando la superficie cambie.

import type { UnidigitalClient } from "@devgoliiive/unidigital-sdk";

const fake: UnidigitalClient = {/* el compilador te dice qué falta */};

No se publican mocks ni un doble stub: la resolución legacy no ve subpaths, y meterlos en la raíz arrastraría la herramienta de mocking a las declaraciones o metería código de test en el bundle de producción.

Versionado y publicación

Bump manual, semver independiente, sin gestor de changesets. El major del SDK sigue al major de la API; minor y patch son suyos.

Publicar es subir la versión en packages/sdk/package.json y llevarlo a main: el workflow .github/workflows/publish-sdk.yml verifica el empaquetado y publica. Una versión que el registry ya tiene se salta sin fallar.

Para que un cambio no se quede sin publicar, .github/workflows/sdk-version.yml falla un PR hacia main que toca src/ o tsdown.config.ts sin subir la versión. Sube también SDK_VERSION en src/version.ts; un test exige que coincidan.

El workflow se autentica con trusted publishing (OIDC): npmjs.com confía en ese workflow de este repo, y no hay token guardado. La configuración vive en npmjs.com, en Settings → Trusted Publisher del paquete.

Para publicar a mano, inicia sesión con npm login como miembro de la organización devgoliiive:

pnpm --filter @devgoliiive/unidigital-sdk release

版本与发布记录

@devgoliiive/unidigital-sdk 共发布 5 个版本,首次发布于 2026/09/26;过去 12 个月发布了 5 个版本。

版本发布日期距今
0.1.4latest2026/10/1330 天前
0.1.32026/10/091 个月前
0.1.22026/10/071 个月前
0.1.12026/09/301 个月前
0.1.02026/09/261 个月前

关于 @devgoliiive/unidigital-sdk 的常见问题

@devgoliiive/unidigital-sdk 是什么?

Typed client for the Unidigital fiscal billing API, with webhook signature verification.

如何安装 @devgoliiive/unidigital-sdk?

在项目目录运行 npm install @devgoliiive/unidigital-sdk,或使用 pnpm add / yarn add / bun add。

@devgoliiive/unidigital-sdk 支持 TypeScript 吗?

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

@devgoliiive/unidigital-sdk 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@devgoliiive/unidigital-sdk 可以商用吗?

@devgoliiive/unidigital-sdk 使用 UNLICENSED 许可。作者声明为 UNLICENSED,即不授予公开使用许可(与公共领域的 Unlicense 不同)。使用前需获得作者授权。以上为概括,不构成法律意见,请以许可原文为准。

@devgoliiive/unidigital-sdk 的下载量有多少?

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

@devgoliiive/unidigital-sdk 还在维护吗?

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