Skip to content

Documentos

Los seis tipos comparten la misma forma. Lo que cambia es qué campos piden y qué acciones aceptan.

RecursoDocumentoKuDEAcciones propias
sd.invoicesFacturanominate, confirmDraft, discardDraft
sd.receiptsReciboconfirmDraft, discardDraft
sd.creditNotesNota de crédito
sd.debitNotesNota de débito
sd.remissionNotesNota de remisión
sd.autoInvoicesAutofactura

Todos aceptan cancel y resendEmail. Todos menos el recibo aceptan además resendToSet, que reenvía a la DNIT un documento que quedó en error.

Establecimiento y punto de expedición

Se pueden dar por su código —el que va impreso en el documento— o por el id interno de SmartDoc:

ts
new Client({ establishment: '001', dispatchPoint: '001' });   // por código
new Client({ establishment: 8, dispatchPoint: 14 });          // por id interno

También por llamada, que es lo que corresponde si emitís desde varias sucursales:

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

Si no se indica ninguno, el SDK los descubre solo cuando el contribuyente tiene exactamente uno. Si hay más, lanza ConfigurationError con las opciones disponibles en vez de elegir: el código elegido queda impreso en el documento y en su CDC, y equivocarlo obliga a anular y reemitir.

Crear y editar

create() emite; update() reemplaza. No es un parche: hay que pasar el documento entero, porque la API valida el cuerpo completo igual que al crear.

ts
let factura = await sd.invoices.create({ recipient, items });
factura = await sd.invoices.update(factura.id, { recipient, items });

Solo antes de subir a la DNIT

Una vez que el documento pasó a uploaded_to_set, la API rechaza la edición. A partir de ahí la vía es anular y reemitir.

Ítems y montos

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

new Item('Consultoría', {
  quantity: 2,
  unitAmount: 550_000,          // con el IVA ya incluido
  iva: Iva.TEN,                 // por omisión
  code: 'SKU-123',              // si no se pasa, se numera por posición
  measureUnit: MeasureUnit.UNIT,
  discount: 10_000,
});

La descripción también puede ir dentro del objeto, si te resulta más cómodo:

ts
new Item({ description: 'Consultoría', quantity: 2, unitAmount: 550_000 });

El descuento es por unidad, no de la línea

Un discount de 10.000 sobre 3 unidades descuenta 30.000 en total.

El SDK calcula amount y el desglose completo de IVA, contemplando el descuento por ítem, el global y el de SEDECO. Los montos se calculan con aritmética decimal exacta, no con punto flotante.

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

totalsFor([new Item('X', { quantity: 1, unitAmount: 1_100_000 })]);
// { amount: 1100000, amount10Percent: 1100000,
//   taxed10Percent: 1000000, iva10Percent: 100000 }

IVA mixto

Cuando parte de la línea está gravada y parte exenta:

ts
new Item('Combo', {
  quantity: 1,
  unitAmount: 1_000_000,
  iva: Iva.MIXED_TEN,
  ivaBase: 60,          // 60% gravado al 10%, 40% exento
});

El receptor

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

Recipient.entity({ ruc: '80012345-1', socialName: 'Cliente S.A.' });
Recipient.person({ ruc: '5000001-1', name: 'Juan Pérez' });
Recipient.notNominated();
Recipient.foreign({ name: 'Cliente do Brasil', countryCode: 'BRA' });
Recipient.government({ ruc: '...', socialName: '...' });
Recipient.diplomatic({ ruc: '...', socialName: '...' });

Cada constructor se expande a los campos client* que corresponden, con sus reglas condicionales.

Direcciones

Un documento cuyo receptor lleva dirección necesita seis campos: departamento, distrito y ciudad, cada uno con código y descripción. El SDK embebe el catálogo geográfico, así que una búsqueda por nombre los resuelve:

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

Recipient.person({
  ruc: '5000001-1',
  name: 'Juan Pérez',
  address: 'Av. España 1234',
  houseNumber: 1234,
  city: geo.findCity('Asunción'),
});

Cuando el nombre es ambiguo, findCity lanza ValidationError con las opciones en vez de elegir una:

ts
geo.findCity('Encarnación');
// ValidationError: "Encarnación" es ambiguo: hay 2 ciudades con ese nombre...

geo.findCity('Concepción', { district: 'SAN CARLOS' });   // desempata
geo.city(2226);                                           // o por código

El receptor extranjero es el único que puede llevar dirección sin códigos geográficos paraguayos.

Estados

Los seis tipos comparten el mismo vocabulario. El flujo habitual:

pending → generated → uploaded_to_set → approved_by_set
ts
factura.status          // 'approved_by_set'
factura.isApproved
factura.isTerminal
factura.isError
factura.errorCode       // undefined si el documento no está en error
factura.errorMessage

Preguntá por las propiedades, no por el estado

isApproved, isTerminal e isError conocen las reglas de cada tipo de documento. Comparar contra estados a mano es más frágil.

recoverable_error no es un estado final

SmartDoc lo reintenta por su cuenta, así que el documento todavía puede llegar a aprobarse. waitUntilFinal no corta ahí.

Para el recibo, generated es el estado final de éxito

El recibo no se envía a la DNIT, así que nunca llega a approved_by_set. isApproved e isTerminal ya lo contemplan.

Esperar

ts
const factura = await sd.invoices.waitUntilFinal(id);

Consulta hasta llegar a un estado terminal, respetando el límite de tasa. Acepta timeout y pollInterval; si se agota el plazo lanza TimeoutError, que no significa que el documento haya fallado sino que se acabó la espera.

ts
await sd.invoices.waitUntilFinal(id, { timeout: 600, pollInterval: 5 });

En producción conviene escuchar webhooks en lugar de esto.

Borradores

Facturas y recibos se pueden crear en borrador: quedan guardados y no se envían a la DNIT hasta confirmarlos.

ts
const borrador = await sd.invoices.create({ recipient, items, draft: true });

await sd.invoices.confirmDraft(borrador.id);     // ahora sí sale
await sd.invoices.discardDraft(borrador.id);     // o se descarta

Acciones

ts
await sd.invoices.cancel(factura.id, 'Error en el monto');
await sd.invoices.resendEmail(factura.id);
await sd.invoices.resendToSet(factura.id);

Estas acciones no devuelven el documento: se ejecutan de forma asíncrona, así que para ver cómo quedó hay que volver a consultarlo.

ts
await sd.invoices.cancel(factura.id, 'Error en el monto');
const anulada = await sd.invoices.waitUntilFinal(factura.id);   // 'cancelled'

Anular exige que el documento esté aprobado y que el motivo no vaya vacío. Si el estado no lo permite, la API responde ACTION_NOT_ALLOWED.

Facturas innominadas

Venta de mostrador sin identificar al comprador:

ts
const factura = await sd.invoices.create({
  recipient: Recipient.notNominated(),
  items,
});

Si después el cliente pide la factura a su nombre, hace falta su ficha en SmartDoc. Si ya la tenés cargada, alcanza con su id; si no, se crea:

Solo se nomina una factura aprobada

Como al anular, la factura tiene que estar aprobada por la DNIT. Antes de eso la API responde ACTION_NOT_ALLOWED. Si acabás de emitirla, esperá el evento invoice.approved —o waitUntilFinal si estás probando— antes de nominarla.

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

const cliente = await sd.clients.create({
  ruc: '80012345-1',
  socialName: 'Cliente S.A.',
  type: ClientType.ENTITY,
  isTaxPayer: true,
  countryCode: 'PRY',
  hasAddress: false,
  email: 'facturas@cliente.com.py',
});

await sd.invoices.nominate(factura.id, cliente.id);

const nominada = await sd.invoices.waitUntilFinal(factura.id);
console.log(nominada.status);        // 'nominated'

Como cancel, nominate no devuelve el documento: la nominación pasa por la DNIT y es asíncrona. La factura queda primero en nomination_requested, un estado intermedio, y termina en nominated —o en nomination_failed si la DNIT la rechaza—. Los datos del cliente aparecen recién cuando llega a nominated.

En producción eso llega por webhook, con el evento invoice.pending mientras está en curso y invoice.approved al completarse.

La ficha del cliente pide más campos que el receptor

sd.clients administra la agenda de clientes de SmartDoc y manda los campos tal como los pide la API, sin los valores por omisión que sí pone Recipient.

Son obligatorios los siete: ruc, socialName, type, isTaxPayer, countryCode, hasAddress y email. Con hasAddress: true se suman address, department, district y city.

Dos cosas que confunden:

  • El campo del tipo se llama type, no clientType.
  • Si falta email, la API responde 500 en lugar de un error de validación. Los otros seis sí llegan como ValidationError con el detalle por campo.

Notas de crédito y débito

Corrigen una factura ya emitida. El documento asociado es obligatorio: sin él la DNIT no sabe sobre qué aplicar la corrección.

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

// El CDC existe recién cuando la factura está aprobada.
const cdc = factura.cdc;
if (!cdc) throw new Error('La factura todavía no tiene CDC.');

await sd.creditNotes.create({
  recipient,
  items: [new Item('Devolución', { quantity: 1, unitAmount: 1_100_000 })],
  associatedDocument: AssociatedDocument.electronic(cdc),
});

Para un documento anterior a la facturación electrónica:

ts
AssociatedDocument.printed({
  stampIdentifier: '12345678',
  establishmentCode: '001',
  dispatchPointCode: '001',
  number: '0000001',
  documentDate: '2024-05-01',
});

Recibos

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

// El CDC de la factura que se está cobrando, guardado al aprobarse.
const cdcDeLaFactura = '01080000001001001000019022026081415828813860';

await sd.receipts.create({
  recipient: Recipient.person({ ruc: '5000001-1', name: 'Juan Pérez' }),
  amount: 1_100_000,
  paymentMethod: ReceiptPaymentMethod.CASH,
  associatedDocument: AssociatedDocument.electronic(cdcDeLaFactura),
});

Aceptan tres tipos de receptor: persona, entidad y extranjero. Sus ítems son más simples —descripción y monto— porque el recibo no liquida impuestos:

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

await sd.receipts.create({
  recipient,
  amount: 1_100_000,
  paymentMethod: ReceiptPaymentMethod.CASH,
  items: [new ReceiptItem('Cobro de la factura 001-001-0000123', 1_100_000)],
});

El documento asociado es opcional: si no se pasa, va AssociatedDocument.none().

Notas de remisión

Amparan un traslado, así que no llevan monto, ni moneda, ni IVA. Lo que sí llevan, y ningún otro documento tiene, son los datos del traslado.

ts
import {
  Carrier,
  RemissionItem,
  TransportEndpoint,
  Vehicle,
  geo,
  Recipient,
} from '@araitek/smartdocjs';

const ciudad = geo.findCity('Luque');

await sd.remissionNotes.create({
  recipient: Recipient.entity({
    ruc: '80012345-1',
    socialName: 'Cliente S.A.',
    address: 'Av. Principal 123',
    houseNumber: 123,
    city: ciudad,
    email: 'cliente@ejemplo.com.py',
  }),
  items: [new RemissionItem({ description: 'Caja de tornillos', quantity: 10 })],
  departure: new TransportEndpoint({
    address: 'Depósito Central',
    city: ciudad,
    houseNumber: 100,
  }),
  arrival: new TransportEndpoint({
    address: 'Sucursal Norte',
    city: ciudad,
    houseNumber: 200,
  }),
  vehicle: new Vehicle({
    brand: 'Toyota',
    type: 'CAMION',
    registrationNumber: 'ABC123',
  }),
  carrier: new Carrier({
    socialName: 'Transporte S.A.',
    ruc: '80099999-1',
    isTaxPayer: true,
    documentNumber: '80099999',
    address: 'Ruta 2 km 15',
    driverName: 'Pedro Gómez',
    driverDocumentNumber: '3456789',
    driverAddress: 'Calle 5 casi 8',
  }),
  estimatedKilometers: 25,
  startDate: '2026-08-06',
  endDate: '2026-08-07',
});

La remisión exige más datos que el resto

Campos que en los otros documentos son opcionales y acá no:

  • El receptor, con dirección completa —address, houseNumber y city— y email.
  • Los dos extremos del traslado, con houseNumber.
  • estimatedKilometers, startDate y endDate.
  • El transportista completo: su documento y dirección, más el nombre, documento y dirección del chofer.

El SDK valida todo esto antes de emitir e indica qué falta.

Los datos del vehículo tienen largo acotado

CampoQué va
typeDescripción libre de 4 a 10 caracteres: 'CAMION', 'Camioneta', 'Furgón'
brandDe 1 a 10 caracteres. 'Mercedes-Benz' no entra: abreviala
registrationNumberLa matrícula, de 6 o 7 caracteres
documentNumberEl número de identificación, de 1 a 20 caracteres

documentType decide cuál de los dos números hay que dar, y el otro se ignora: con REGISTRATION —el valor por omisión— va registrationNumber; con VIN, documentNumber.

type no es transportType: ese es un código y define si el transporte es propio o de terceros.

El motivo condiciona quién puede ser el receptor

Con reason: RemissionReason.BETWEEN_COMPANY_LOCATIONS —traslado entre locales de la misma empresa— el RUC del receptor tiene que coincidir con el del emisor. El motivo por omisión, SALE, no tiene esa restricción.

Autofacturas

Para cuando se le compra a alguien que no puede emitir factura. No hay receptor: va el proveedor, y aparte el lugar donde se hizo la operación.

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

await sd.autoInvoices.create({
  provider: Provider.noRuc({
    fullName: 'Proveedor Informal',
    documentNumber: '1234567',
    address: 'Calle 1',
    city: ciudad,
    houseNumber: 50,
  }),
  items: [new Item('Materia prima', { quantity: 1, unitAmount: 500_000 })],
  transactionAddress: 'Depósito Central',
  transactionCity: ciudad,
  transactionHouseNumber: 100,
  certificateCode: '12345678',        // exactamente 8 caracteres
  certificateNumber: '12345678901',   // exactamente 11 caracteres
});

Son obligatorios el houseNumber del proveedor y el del lugar de la operación, más el código y el número de la constancia, que tienen largo exacto.

Listar

ts
const pagina = await sd.invoices.list({
  status: 'approved_by_set',
  dateFrom: '2026-01-01',
});

for (const factura of pagina) {              // solo esta página
  // ...
}

for await (const factura of pagina.autoPaging()) {   // todas, siguiendo el cursor
  // ...
}

const todas = await pagina.all();            // o de una, en un array

El tamaño por omisión es 20 y el máximo 100. autoPaging() respeta el límite de tasa, así que recorrer un listado largo lleva su tiempo.

dateTo incluye el día que nombra: pedir hasta el 31 de julio trae las facturas del 31, sin importar la hora.

Las fechas son locales, no UTC

Los documentos se fechan en la hora de Paraguay. Si armás la fecha con new Date().toISOString().slice(0, 10) vas a estar pidiendo el día UTC, que después de las 21 h ya es el siguiente, y el listado te va a volver vacío.

ts
const d = new Date();
const hoy = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(
  d.getDate(),
).padStart(2, '0')}`;

await sd.invoices.list({ dateFrom: hoy, dateTo: hoy });

También se puede pasar un Date, que el SDK convierte a hora local. Pero cuidado con la diferencia: como dateTo, un Date corta en ese instante, mientras que la cadena '2026-07-31' llega hasta el final del día. Para pedir un día entero, usá la cadena.