Tema
Documentos
Los seis tipos comparten la misma forma. Lo que cambia es qué campos piden y qué acciones aceptan.
| Recurso | Documento | KuDE | Acciones propias |
|---|---|---|---|
sd.invoices | Factura | ✓ | nominate, confirmDraft, discardDraft |
sd.receipts | Recibo | ✓ | confirmDraft, discardDraft |
sd.creditNotes | Nota de crédito | ✓ | — |
sd.debitNotes | Nota de débito | — | — |
sd.remissionNotes | Nota de remisión | — | — |
sd.autoInvoices | Autofactura | — | — |
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 internoTambié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ódigoEl 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_setts
factura.status // 'approved_by_set'
factura.isApproved
factura.isTerminal
factura.isError
factura.errorCode // undefined si el documento no está en error
factura.errorMessagePreguntá 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 descartaAcciones
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, noclientType. - Si falta
email, la API responde 500 en lugar de un error de validación. Los otros seis sí llegan comoValidationErrorcon 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,houseNumberycity— yemail. - Los dos extremos del traslado, con
houseNumber. estimatedKilometers,startDateyendDate.- 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
| Campo | Qué va |
|---|---|
type | Descripción libre de 4 a 10 caracteres: 'CAMION', 'Camioneta', 'Furgón' |
brand | De 1 a 10 caracteres. 'Mercedes-Benz' no entra: abreviala |
registrationNumber | La matrícula, de 6 o 7 caracteres |
documentNumber | El 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 arrayEl 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.
