Skip to content

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;        // equivalente

Emitir 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