Tema
Recetas
Migrar desde la API cruda
Si ya te integraste a mano contra /api/v2, esto es lo que reemplaza el SDK.
js
const { randomUUID } = require('node:crypto');
const headers = {
Authorization: `Bearer ${API_KEY}`,
'Idempotency-Key': randomUUID(),
'Content-Type': 'application/json',
};
const contribuyente = (await (await fetch(`${BASE}/taxpayer`, { headers })).json()).data;
const timbrados = (await (await fetch(`${BASE}/stamps?enabled=true`, { headers })).json()).data;
const timbrado = timbrados[0];
const total = 1100000;
const iva = Math.round(total / 11);
const cuerpo = {
taxPayerRuc: contribuyente.ruc,
taxPayerSocialName: contribuyente.socialName,
taxPayerFantasyName: contribuyente.fantasyName,
stampIdentifier: timbrado.identifier,
stampBeginDate: timbrado.beginDate.slice(0, 10),
establishmentCode: '001',
dispatchPointIdentifier: '001',
invoiceDate: '2026-06-15T10:00:00',
saleType: 'cash',
transactionTypeCode: 1,
currency: 'PYG',
clientType: 'entity',
clientRuc: '80012345-1',
clientSocialName: 'Cliente S.A.',
clientIsTaxPayer: true,
clientCountryCode: 'PRY',
clientHasAddress: false,
amount: total,
amount10Percent: total,
taxed10Percent: total - iva,
iva10Percent: iva,
items: [
{
description: 'Consultoría',
code: '1',
measureUnit: 77,
quantity: 1,
unitAmount: total,
totalAmount: total,
ivaType: '10_percent',
amount10Percent: total,
taxed10Percent: total - iva,
iva10Percent: iva,
},
],
};
const r = await fetch(`${BASE}/invoices`, {
method: 'POST',
headers,
body: JSON.stringify(cuerpo),
});
if (!r.ok) throw new Error(await r.text());
const factura = (await r.json()).data;ts
import { Client, Item, Recipient, TransactionType } from '@araitek/smartdocjs';
const sd = new Client({ establishment: '001', dispatchPoint: '001' });
const factura = await sd.invoices.create({
recipient: Recipient.entity({ ruc: '80012345-1', socialName: 'Cliente S.A.' }),
items: [new Item('Consultoría', { quantity: 1, unitAmount: 1_100_000 })],
transactionType: TransactionType.MERCHANDISE_SALE,
invoiceDate: '2026-06-15T10:00:00',
});Mandar campos que el SDK no modela
Para un campo de la API que no tiene parámetro propio, pasalo en extra: va al cuerpo sin tocar.
ts
await sd.invoices.create({
recipient,
items,
extra: {
dncpContractCode: 'ABC-123',
dncpContractYear: '2026',
},
});Leer un campo de la respuesta cruda
Para un campo que el modelo no expone como propiedad:
ts
factura.get('invoiceNumber');
factura.raw.invoiceNumber; // equivalenteEmitir por lotes
Para emitir muchos documentos seguidos. El SDK regula el ritmo solo; hay que decidir qué hacer con los que fallan.
ts
import {
RateLimitError,
ServerError,
ValidationError,
type Client,
} from '@araitek/smartdocjs';
async function emitirLote(sd: Client, ventas: Venta[]) {
const emitidas: Array<[number, number]> = [];
const fallidas: Array<[number, string]> = [];
for (const venta of ventas) {
try {
const factura = await sd.invoices.create({
recipient: recipientFor(venta.cliente),
items: itemsDe(venta),
});
emitidas.push([venta.id, factura.id]);
} catch (error) {
if (error instanceof ValidationError) {
fallidas.push([venta.id, error.message]);
} else if (error instanceof RateLimitError || error instanceof ServerError) {
await cola.reintentar(venta.id, error.message);
} else {
throw error;
}
}
}
return { emitidas, fallidas };
}En paralelo, dejando que el regulador de tasa ordene las salidas:
ts
const resultados = await Promise.allSettled(
ventas.map((venta) =>
sd.invoices.create({
recipient: recipientFor(venta.cliente),
items: itemsDe(venta),
}),
),
);El límite es de 5 solicitudes por segundo por API Key, así que Promise.all con mil ventas no va más rápido: el SDK las espacia igual.
Exportar un mes de facturación
Para bajar todas las facturas de un período a un archivo.
ts
import { createWriteStream } from 'node:fs';
const salida = createWriteStream('julio.csv');
salida.write('numero,fecha,cliente,monto,cdc\n');
const pagina = await sd.invoices.list({
status: 'approved_by_set',
dateFrom: '2026-07-01',
dateTo: '2026-07-31',
});
for await (const factura of pagina.autoPaging()) {
salida.write(
[
factura.get('invoiceNumber'),
String(factura.get('invoiceDate')).slice(0, 10),
factura.get('clientSocialName'),
factura.amount,
factura.cdc,
].join(',') + '\n',
);
}
salida.end();dateTo incluye el día que nombra: el 31 de julio entra en el listado.
Emitir desde varias sucursales
Si el contribuyente tiene más de un establecimiento, no lo fijes en el cliente: pasalo en cada llamada.
ts
const sd = new Client(); // sin establishment ni dispatchPoint
await sd.invoices.create({
recipient,
items,
establishment: sucursal.codigoEstablecimiento,
dispatchPoint: caja.codigoPunto,
});Sin indicarlo y con más de uno, la llamada lanza ConfigurationError con las opciones disponibles.
Verificar una factura de proveedor
Para consultar en la DNIT un documento que recibiste, antes de cargarlo. No lo registra.
ts
const documento = await sd.receivedDocuments.lookupByCdc('01800695631001...');
console.log(documento.get('status'), documento.get('taxPayerRuc'));Requiere el permiso write_received_documents. Si el documento no existe en la DNIT, la llamada lanza ValidationError con código SIFEN_REJECTED.
Usar tu propia clave de idempotencia
Cada create() ya va con una clave nueva que genera el SDK, así que esto es opcional. Sirve si preferís que la clave venga de tu sistema.
ts
const factura = await sd.invoices.create({
recipient: recipientFor(venta.cliente),
items: itemsDe(venta),
idempotencyKey: `venta-${venta.id}`,
});Repetir la llamada con la misma clave devuelve el documento ya creado.
Conciliar contra tu base
Guardá el id de SmartDoc, que es lo que llega en el webhook:
ts
const factura = await sd.invoices.create({ recipient, items });
venta.smartdocId = factura.id;
await venta.save();Para el camino inverso, poné tu identificador en code, que queda impreso en el documento. Tiene que medir entre 3 y 15 caracteres:
ts
Recipient.entity({
ruc: '80012345-1',
socialName: 'Cliente S.A.',
code: `cli-${clienteLocal.id}`,
});Ver el cuerpo sin mandarlo
Los armadores de cuerpo son funciones puras y no tocan la red:
ts
import { Item, Recipient, invoiceBody } from '@araitek/smartdocjs';
const cuerpo = invoiceBody(
{ taxPayerRuc: '80012345-1' }, // base del emisor, a mano
{
recipient: Recipient.entity({ ruc: '8-1', socialName: 'C' }),
items: [new Item('X', { quantity: 1, unitAmount: 1_100_000 })],
},
);
console.log(JSON.stringify(cuerpo, null, 2));Para ver lo que realmente sale por la red, envolvé el fetch del cliente:
ts
const sd = new Client({
fetch: async (url, init) => {
console.log(init.method, url, init.body);
return await fetch(url, init);
},
});Usar el SDK desde TypeScript
Los tipos vienen en el paquete: no hace falta instalar @types de nada.
ts
import type { CreateInvoiceOptions, Document, Page } from '@araitek/smartdocjs';
async function emitir(opciones: CreateInvoiceOptions): Promise<Document> {
return await sd.invoices.create(opciones);
}
const pagina: Page<Document> = await sd.invoices.list({ limit: 50 });Los catálogos son uniones de literales, así que el compilador rechaza un valor inválido antes de correr:
ts
import { SaleType } from '@araitek/smartdocjs';
const tipo: SaleType = 'cash'; // ✓
const otro: SaleType = 'Contado'; // ✗ error de compilación