Skip to content

Cómo integrar

El código que vas a escribir. Ejemplos completos de cada operación, para copiar y adaptar.

Cuando llegues a una decisión que no sea obvia —cómo traducir tu modelo al documento, qué tratamiento de IVA lleva cada producto, qué revisar antes de producción— eso está en Adaptar tu sistema.

Qué tenés que implementar

Tres piezas. Con eso ya facturás electrónicamente:

#Qué construísCuándo corre
1Una llamada de creaciónCuando se genera la venta en tu sistema
2Un endpoint HTTP que reciba los webhooks, con dos handlersCuando SmartDoc avisa cómo salió
3Una acción de correcciónSolo si el documento quedó en error

Emitir es asíncrono: la llamada de creación vuelve enseguida y la aprobación llega después, por webhook. Ese es el flujo:

1. Se genera la venta
   └─> sd.invoices.create(...)          guardás id + tipo, estado "pendiente"

2. Llega el webhook
   ├─ invoice.approved                  guardás el CDC, estado "aprobada"  ✔ fin
   └─ invoice.error                     guardás el motivo, estado "error"

3. Corregís los datos                     │
   └─> sd.invoices.update(...)  <─────────┘
         └─> vuelve al paso 2

En detalle, lo mínimo de cada pieza:

  1. Al crear, guardá el id del documento y su tipo ('invoice', 'receipt'…). Es lo que va a llegar en el webhook y sin eso no podés vincular el evento con tu venta.
  2. El endpoint de webhooks es una ruta POST de tu aplicación, expuesta en internet con https://, cuya URL registrás en el panel de SmartDoc. Ahí dentro, dos handlers:
    • Con el evento de aprobación, guardá el CDC y marcá la venta como aprobada. El CDC solo llega acá, y es obligatorio si más adelante necesitás emitir un recibo o una nota de crédito o débito sobre esa factura.
    • Con el evento de error, guardá errorCode y errorMessage, y mostralo donde tu equipo lo vea. Un documento en error no se resuelve solo.
  3. Para corregir, una acción en tu sistema que vuelva a mandar el documento con los datos arreglados usando update. SmartDoc lo reprocesa y vuelve a avisarte por webhook.

De ahí en adelante es tuyo: cobrar con recibos, corregir con notas, anular. Los ejemplos de abajo cubren cada caso.

Antes de empezar

Dos variables en el entorno:

bash
export SMARTDOC_API_KEY="pk_..."
export SMARTDOC_WEBHOOK_SECRET="..."

Y las dos piezas del SDK que se usan en todos los ejemplos:

ts
import { Client, Webhooks } from '@araitek/smartdocjs';

export const sd = new Client();
export const webhooks = new Webhooks({
  secret: process.env.SMARTDOC_WEBHOOK_SECRET,
});

Qué es del SDK y qué es tuyo

Todo lo que se importa de @araitek/smartdocjs viene del paquete. El resto es de tu aplicación y lo vas a renombrar por lo que uses:

En los ejemplosQué representa
ventaEl registro de la operación en tu base de datos
VentaCómo consultás esa tabla
venta.smartdocEntity, venta.smartdocId, venta.cdc, venta.estadoFiscalCampos que agregás a tu tabla para guardar lo que devuelve SmartDoc

Todo lleva await

No hay cliente sincrónico: todas las llamadas a la API devuelven una promesa.

Montar el endpoint de webhooks

Los webhooks.on(...) de los ejemplos registran handlers, pero por sí solos no reciben nada. Hace falta exponer una ruta POST en tu aplicación y conectarla al SDK, que se encarga de verificar la firma y llamar al handler que corresponda.

Abajo están los frameworks más usados, que el SDK trae resueltos. No estás limitado a ellos: sirve cualquiera, y no hace falta tener ninguno instalado.

ts
import express from 'express';

const app = express();

// El cuerpo tiene que llegar crudo: la firma se calcula sobre esos bytes.
app.post(
  '/webhooks/smartdoc',
  express.raw({ type: 'application/json' }),
  webhooks.expressHandler(),
);
ts
import Fastify from 'fastify';

const app = Fastify();

// Fastify parsea el JSON por omisión; acá se pide el cuerpo sin tocar.
app.addContentTypeParser(
  'application/json',
  { parseAs: 'buffer' },
  (_request, body, done) => done(null, body),
);

app.post('/webhooks/smartdoc', webhooks.fastifyHandler());
ts
// Cualquier runtime con Request y Response estándar.
const handler = webhooks.fetchHandler();

// Hono
app.post('/webhooks/smartdoc', (c) => handler(c.req.raw));

// Next.js App Router — app/webhooks/smartdoc/route.ts
export const POST = handler;
ts
// Pasale a dispatch el cuerpo crudo y los encabezados. Verifica la firma,
// descarta las entregas repetidas y llama al handler que corresponda.
import { SignatureError } from '@araitek/smartdocjs';

async function recibir(cuerpoCrudo, encabezados) {
  try {
    await webhooks.dispatch(cuerpoCrudo, encabezados);
  } catch (error) {
    if (error instanceof SignatureError) return { status: 400 };
    throw error;
  }
  return { status: 200 };
}
ts
// Servidor mínimo, para ver eventos llegar sin montar una aplicación.
const server = await webhooks.serve({ port: 3000 });

// ...
await server.close();

El cuerpo tiene que ser el crudo

La firma se calcula sobre los bytes tal como llegaron. Si el framework parsea el JSON y lo volvés a serializar, la firma deja de coincidir. Los adaptadores del SDK lanzan SignatureError con la explicación si detectan un cuerpo ya parseado.

Después, en el panel de SmartDoc: Conectividad → Webhooks → Agregar, con la URL pública de esa ruta —https://tu-servidor.com/webhooks/smartdoc— y los tipos de evento a los que te suscribís. Al guardar, el panel muestra una sola vez el secreto: ese es el que va en new Webhooks({ secret }).

Para probar en tu máquina

La URL registrada tiene que ser pública y https://, así que localhost no sirve. Levantá un túnel y registrá la URL que devuelva:

bash
ngrok http 3000

Cada endpoint tiene un botón Enviar evento de prueba que verifica la conectividad y la firma sin emitir nada.

El detalle de la firma, los reintentos y la deduplicación está en Webhooks.

Emitir una factura

ts
import { Item, Recipient } from '@araitek/smartdocjs';

const factura = await sd.invoices.create({
  recipient: Recipient.entity({
    ruc: '80012345-1',
    socialName: 'Cliente Ejemplo S.A.',
    email: 'facturas@cliente.com.py',
  }),
  items: [
    new Item('Consultoría de agosto', { quantity: 1, unitAmount: 1_100_000 }),
  ],
});

venta.smartdocEntity = 'invoice';
venta.smartdocId = factura.id;
venta.estadoFiscal = String(factura.status);   // pending o generated
await venta.save();

La aprobación llega al webhook:

ts
import { Event } from '@araitek/smartdocjs';

webhooks.on(Event.INVOICE_APPROVED, async (evento) => {
  const [entidad, numero] = evento.documentKey;   // ['invoice', 1234]

  const venta = await Venta.findOne({ smartdocEntity: entidad, smartdocId: numero });
  venta.cdc = evento.cdc;                         // ← guardalo
  venta.estadoFiscal = 'aprobada';
  await venta.save();
});

Guardá el CDC. Solo llega acá, y es obligatorio si más adelante necesitás emitir un recibo, una nota de crédito o una nota de débito sobre esta factura: esos documentos la referencian por su CDC. Sin él no los podés emitir.

Guardá también el par [entityType, entityId], no solo el número: el id se numera por tipo de documento, así que el recibo 20 y la nota de débito 20 existen a la vez.

Emitir un recibo de esa factura

El recibo se emite sobre una factura ya aprobada, y la referencia por su CDC.

ts
import { AssociatedDocument, ReceiptPaymentMethod, Recipient } from '@araitek/smartdocjs';

const recibo = await sd.receipts.create({
  recipient: Recipient.entity({
    ruc: '80012345-1',
    socialName: 'Cliente Ejemplo S.A.',
    email: 'facturas@cliente.com.py',
  }),
  amount: 1_100_000,
  paymentMethod: ReceiptPaymentMethod.CASH,
  associatedDocument: AssociatedDocument.electronic(venta.cdc),
  invoiceId: venta.smartdocId,
});
ts
webhooks.on(Event.RECEIPT_APPROVED, async (evento) => {
  const [entidad, numero] = evento.documentKey;
  await Cobro.update(
    { smartdocEntity: entidad, smartdocId: numero },
    { estadoFiscal: 'generado' },
  );
});

El recibo no se envía a la DNIT: su estado final de éxito es generated y evento.cdc viene en undefined.

Con cheque hacen falta dos datos más:

ts
await sd.receipts.create({
  // ...
  paymentMethod: ReceiptPaymentMethod.CHECK,
  checkBank: 'Banco Continental',
  checkNumber: '12345678',
});

Para un recibo suelto, sin factura asociada:

ts
associatedDocument: AssociatedDocument.none(),

Emitir una nota de crédito

Se emite sobre una factura ya aprobada para disminuir su monto: una devolución, un descuento posterior. Lleva documento asociado obligatorio, y también la referencia por CDC.

ts
const nota = await sd.creditNotes.create({
  recipient: Recipient.entity({
    ruc: '80012345-1',
    socialName: 'Cliente Ejemplo S.A.',
    email: 'facturas@cliente.com.py',
  }),
  items: [
    new Item('Devolución de mercadería', { quantity: 1, unitAmount: 110_000 }),
  ],
  associatedDocument: AssociatedDocument.electronic(venta.cdc),
  invoiceId: venta.smartdocId,
});
ts
webhooks.on(Event.CREDIT_NOTE_APPROVED, async (evento) => {
  const [entidad, numero] = evento.documentKey;
  const nota = await NotaCredito.findOne({
    smartdocEntity: entidad,
    smartdocId: numero,
  });
  nota.cdc = evento.cdc;
  await nota.save();
});

La nota de débito se emite igual, con sd.debitNotes, y aumenta el monto de la factura —intereses, gastos—.

Anular una factura

Solo se anula una factura aprobada, y el motivo es obligatorio.

ts
await sd.invoices.cancel(venta.smartdocId, 'Error en el monto facturado');
venta.estadoFiscal = 'anulación solicitada';
await venta.save();
ts
webhooks.on(Event.INVOICE_CANCELLED, async (evento) => {
  const [entidad, numero] = evento.documentKey;
  await Venta.update(
    { smartdocEntity: entidad, smartdocId: numero },
    { estadoFiscal: 'anulada' },
  );
});

cancel no devuelve el documento: la anulación es asíncrona y se confirma por el evento.

Si la factura todavía no está aprobada, la llamada lanza un ValidationError con código ACTION_NOT_ALLOWED.

Corregir una factura que quedó en error

Si la DNIT la rechaza o falla por cualquier motivo, el documento queda en error y llega un evento de error. Se corrige editándolo: SmartDoc lo vuelve a procesar solo, sin reenviarlo ni reemitirlo.

ts
webhooks.on(Event.INVOICE_ERROR, async (evento) => {
  const [entidad, numero] = evento.documentKey;
  const venta = await Venta.findOne({ smartdocEntity: entidad, smartdocId: numero });

  venta.estadoFiscal = 'error';
  venta.errorFiscal = `${evento.errorCode}: ${evento.errorMessage}`;
  await venta.save();

  avisarAlEquipo(venta);
});

Y una vez corregido el dato que corresponda:

ts
await sd.invoices.update(venta.smartdocId, {
  recipient: Recipient.entity({
    ruc: '80012345-1',                        // el dato corregido
    socialName: 'Cliente Ejemplo S.A.',
    email: 'facturas@cliente.com.py',
  }),
  items: [
    new Item('Consultoría de agosto', { quantity: 1, unitAmount: 1_100_000 }),
  ],
});

Vuelve a procesarse y llega invoice.approved como en el camino normal.

update no es un parche: hay que pasar el documento entero, igual que al crearlo. Lo que no mandes, se pierde.

Se puede editar en cualquier estado menos uploaded_to_set y approved_by_set. Ahí la API responde EDIT_NOT_ALLOWED y la vía es anular y reemitir.

Identificar al receptor: por datos o por id

Los datos del receptor van siempre, porque son los que se imprimen en el documento:

ts
Recipient.entity({
  ruc: '80012345-1',
  socialName: 'Cliente Ejemplo S.A.',
  email: 'facturas@cliente.com.py',
});

Si además tenés el cliente cargado en SmartDoc, agregá su clientId para que el documento quede asociado a esa ficha en el panel:

ts
import { ClientType, Recipient } from '@araitek/smartdocjs';

Recipient.fromClientId(196, {                 // id del cliente en SmartDoc
  clientType: ClientType.ENTITY,
  ruc: '80012345-1',
  socialName: 'Cliente Ejemplo S.A.',
  isTaxPayer: true,
});

El clientId no reemplaza los datos: los complementa.

Elegir establecimiento y punto de expedición

Hace falta solo si el contribuyente tiene más de uno. Se aceptan las dos formas, y son equivalentes:

ts
// Por código, el que se ve en el documento
new Client({ establishment: '001', dispatchPoint: '001' });

// Por id interno de SmartDoc
new Client({ establishment: 8, dispatchPoint: 14 });

Si emitís desde varias sucursales, pasalos en cada llamada en vez de fijarlos:

ts
await sd.invoices.create({
  recipient,
  items,
  establishment: '002',
  dispatchPoint: '001',
});

Sin indicarlos y con más de uno, la llamada lanza ConfigurationError con las opciones disponibles.

Si todavía no tenés webhooks montados

Para una prueba rápida o un script de una sola corrida, waitUntilFinal consulta hasta que el documento llegue a un estado final:

ts
const final = await sd.invoices.waitUntilFinal(factura.id);
console.log(final.status, final.cdc);

Sirve para probar. En producción usá webhooks: el polling consume cuota del límite de solicitudes por segundo, y una espera puede llevar minutos.

Y después

  • Adaptar tu sistema — traducir tu modelo de datos al del documento, el criterio del IVA por producto y el checklist de producción.
  • Webhooks — la firma, los reintentos y la deduplicación.
  • Errores — qué error conviene reintentar y cuál no.
  • Documentos — qué pide cada tipo y qué acciones acepta.