Inicio rápido
Para probar sin crear cuenta, usá la clave demo pública sk_test_PTBJ4kRP.bI1MOdIBMdFJ2lUVhPbHbPHKpfMkr2kj5uM-lKb9WTs: ya va en el ejemplo de abajo y en la colección Postman. Emite en el Ambiente de Pruebas de la DNIT, sin validez fiscal. Es compartida — todos ven los documentos de todos —, así que no uses datos reales de clientes. Webhooks y datos propios piden la clave de tu cuenta.
Con la clave de tu cuenta, antes de la primera emisión hacen falta tres cosas en el panel: el timbrado con su establecimiento y punto de expedición, el certificado .p12 y el CSC de la DNIT. Sin cualquiera de ellos la API responde 422 antes de gastar numeración — nunca queda un número asignado a un documento que no puede transmitirse.
curl -X POST https://api.timbrale.com.py/v1/documentos \
-H "Authorization: Bearer sk_test_PTBJ4kRP.bI1MOdIBMdFJ2lUVhPbHbPHKpfMkr2kj5uM-lKb9WTs" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"tipoDocumento": "factura",
"establecimiento": "001",
"puntoExpedicion": "001",
"moneda": "PYG",
"tipoTransaccion": 2,
"condicion": {
"tipo": 1,
"entregas": [
{
"tipo": 1,
"monto": "450000",
"moneda": "PYG"
}
]
},
"factura": {
"presencia": 1
},
"cliente": {
"contribuyente": true,
"ruc": "80069590-9",
"razonSocial": "EMPRESA DE PRUEBA S.A.",
"tipoOperacion": 1
},
"items": [
{
"codigo": "SRV-01",
"descripcion": "Servicio mensual",
"cantidad": 1,
"precioUnitario": "450000",
"iva": 10
}
]
}'202 Accepted
{
"id": "0f3a1c2e-...",
"cdc": "01800123450001001000000112026...",
"tipoDocumento": "factura",
"numeroCompleto": "001-001-0000001",
"status": "queued",
"fechaEmision": "2026-09-10T14:03:11.000Z",
"moneda": "PYG",
"total": "450000",
"ambiente": "test",
"qrLink": null,
"ultimoError": null,
"ultimoIntentoAt": null,
"intentos": 0
}total es la suma de cantidad × precioUnitario menos los descuentos: el precio unitario ya incluye el IVA y el total no le suma nada encima.
202 significa que el documento existe, tiene número y CDC, y quedó en cola para firma y transmisión. El veredicto del SIFEN llega después: se consulta con GET /v1/documentos/:id hasta que el status sea approved o rejected, o llega solo por webhook.
Autenticación
Todas las rutas exigen el header Authorization: Bearer sk_.... Las claves se crean en Panel → Claves de API y el secreto se muestra una sola vez.
| Prefijo | Ambiente | Qué hace |
|---|---|---|
| sk_test_ | sandbox | emite contra el ambiente de prueba del SIFEN; no consume cuota, disponible en todos los planes |
| sk_prod_ | producción | emite y consulta documentos con validez fiscal; exige un plan con API |
El ambiente es el de la clave, no un parámetro: una clave de sandbox nunca ve ni emite documentos de producción, ni siquiera pasando ?ambiente=prod. Sin un plan con API, toda llamada con clave de producción —emitir, listar, descargar XML o KuDE, contexto— responde 402; lo emitido se consulta en el panel.
Emitir un documento
El header Idempotency-Key es obligatorio: sin él la respuesta es 400. Reenviar la misma clave devuelve 200 con el documento anterior en lugar de emitir otro — un timeout de red no duplica una factura. Emisión nueva devuelve 202.
| Header | Obligatorio | Qué hace |
|---|---|---|
| Authorization | sí | Bearer sk_test_… o sk_prod_… |
| Idempotency-Key | sí | cualquier string único por documento — un UUID sirve |
| Content-Type | sí | application/json |
| Accept-Language | no | es, pt o en; define el idioma de los mensajes de error |
| Prefer | no | wait=N (hasta 30): espera hasta N segundos el veredicto del SIFEN |
Con Prefer: wait=10 la respuesta espera el veredicto: si el SIFEN responde dentro del plazo, devuelve 201 con el documento completo (approved o rejected, igual que el GET); si no, el mismo 202 de siempre y el veredicto llega después. La integración tiene que tratar los dos. Sin el header nada cambia; modo: "lote" no espera.
modo: "lote" deja el documento en queued hasta que se cierre el lote, y el cierre hoy sale del panel, no de la clave de API. Si se emite por lote y nadie lo cierra, el número queda asignado y el documento nunca llega al SIFEN. Con sincrono (el default) el documento sale solo, apenas se emite, y no hay nada que cerrar. En los dos casos la transmisión es por el WS asíncrono del SIFEN: el veredicto no viene en la respuesta, llega en segundos o minutos, por webhook o consultando el documento.Campos del documento
El cuerpo se valida entero antes de tocar la numeración. En la raíz, en cliente y en items[] los campos desconocidos se rechazan: si un nombre está mal escrito, la respuesta lo dice en vez de ignorarlo. Dentro de condicion se descartan en silencio.
Raíz
| Campo | Tipo | Obligatorio | Nota |
|---|---|---|---|
| tipoDocumento | enum | sí | factura, autofactura, notaCredito, notaDebito, notaRemision |
| establecimiento | string(3) | sí | código del establecimiento, 3 dígitos: "001" |
| puntoExpedicion | string(3) | sí | punto de expedición, 3 dígitos |
| fecha | date | no | omitida, vale el instante de la emisión. Sin zona horaria (`2026-09-30` o `2026-09-30T23:59:59`) es la hora de Asunción del emisor. Hasta 720 h (30 días) atrás y 120 h (5 días) adelante: fuera de eso el SIFEN rechaza (1150/1151) y la API responde 422 sin gastar numeración |
| tipoEmision | int 1-2 | no | 1 normal (default), 2 contingencia |
| tipoTransaccion | int 1-13 | no | omitido, cae en el configurado en la empresa; si tampoco está, 422 |
| tipoImpuesto | int 1-5 | no | 1 IVA (default), 2 ISC, 3 renta, 4 ninguno, 5 IVA-renta |
| moneda | enum | no | PYG (default), USD, BRL, ARS, EUR |
| cambio | string | number | condicional | obligatorio cuando moneda ≠ PYG |
| condicion | objeto | condicional | obligatorio salvo en notaRemision |
| cliente | objeto | sí | ver abajo |
| items | array | sí | al menos uno |
| factura | { presencia } | condicional | obligatorio en factura: presencia 1-9 — 1 presencial, 2 electrónica, 3 telemarketing… |
| autoFactura | objeto | condicional | obligatorio en autofactura: datos del vendedor no contribuyente |
| remision | objeto | condicional | obligatorio en notaRemision: motivo, tipoResponsable, kms |
| detalleTransporte | objeto | condicional | obligatorio en notaRemision, opcional en factura |
| documentoAsociado | objeto | condicional | obligatorio en notaCredito, notaDebito y autofactura |
| notaCreditoDebito | int 1-5 | condicional | motivo: 1 devolución, 2 descuento, 3 bonificación, 4 crédito incobrable… |
| descripcion | string(1000) | no | — |
| observacion | string(1000) | no | — |
| modo | enum | no | sincrono (default) o lote — ver la nota más abajo |
cliente
| Campo | Tipo | Obligatorio | Nota |
|---|---|---|---|
| contribuyente | boolean | sí | true exige ruc con DV válido |
| ruc | string | condicional | formato 80012345-0 (3 a 8 dígitos, guion, DV); el dígito verificador se valida antes de emitir |
| razonSocial | string(255) | sí | — |
| nombreFantasia | string(255) | no | — |
| documentoTipo | int 1-9 | no | 1 CI, 2 pasaporte, 3 cédula extranjera, 9 otro (exige documentoTipoDescripcion) |
| documentoNumero | string(20) | condicional | obligatorio cuando contribuyente = false |
| documentoTipoDescripcion | string(50) | condicional | obligatorio cuando documentoTipo = 9 |
| tipoOperacion | int 1-4 | sí | 1 B2B, 2 B2C, 3 B2G, 4 B2F (exterior, exige pais ≠ PRY) |
| pais | string(3) | no | PRY por default |
| paisDescripcion | string(50) | no | nombre del país en el B2F |
| direccion | string(255) | condicional | obligatoria en notaRemision y cuando tipoOperacion = 4 (B2F): sin ella el SIFEN rechaza con 1318 |
| numeroCasa | string(6) | no | número de puerta; acompaña a direccion |
| ciudad / distrito / departamento | int | no | códigos del catálogo del SIFEN; ignorados cuando tipoOperacion = 4 (la localidad es extranjera) |
| telefono / celular | string(20) | no | — |
| string | no | destino del KuDE por correo |
items[]
| Campo | Tipo | Obligatorio | Nota |
|---|---|---|---|
| codigo | string(50) | sí | código interno del producto o servicio |
| descripcion | string(500) | sí | — |
| cantidad | number > 0 | sí | — |
| precioUnitario | string | number | sí | IVA incluido; se devuelve siempre como string, sin pasar por float |
| descuento | string | number | no | por unidad |
| unidadMedida | int | no | 77 = unidad (default) |
| ivaTipo | int 1-5 | no | 1 gravado (default), 2 exonerado, 3 exento, 4 gravado parcial |
| iva | 0 | 5 | 10 | no | alícuota; 10 por default |
| ivaBase | 0-100 | no | base gravada en porcentaje; 100 en el caso normal |
| agricola | boolean | no | producto agrícola en estado natural; separa las casillas del Formulario 120 |
| fleteExportacion | boolean | no | flete internacional para exportación de bienes; exige ivaTipo 2 o 3 y va a la casilla 153 del Formulario 120 |
| observacion | string(500) | no | — |
| productoId | string(40) | no | id de un producto del catálogo; no va al XML. Con el stock en ventas activado, la factura descuenta stock y la nota de crédito lo devuelve |
condicion
tipo: 1 contado, 2 crédito.
Al contado, entregas[] es obligatorio: una entrada por forma de pago, con tipo y monto obligatorios y moneda (PYG por default) y cambio opcionales — la suma de los montos es el total del documento. Un contado sin entregas devuelve 422.
Formas de pago en entregas[].tipo: 1 efectivo, 2 cheque, 3 tarjeta de crédito, 4 tarjeta de débito, 5 transferencia, 6 giro, 7 billetera electrónica, 8 tarjeta empresarial, 9 vale, 10 retención, 11 anticipo, 16 pago bancario, 17 pago móvil, 21 pago electrónico, 99 otro (exige tipoDescripcion).
A crédito se agrega credito con tipo (1 plazo, 2 cuotas): el tipo 1 exige plazo (de 2 a 15 caracteres, «30 días»), el tipo 2 exige cuotas. montoEntrega es opcional.
Estados
El documento avanza solo; la integración consulta hasta el estado final. El qrLink apunta a la consulta pública de la DNIT y solo resuelve después de que el SIFEN tiene el documento. ultimoError, ultimoIntentoAt e intentos vienen en cada respuesta y distinguen un queued que está en la cola de uno que quedó parado con error.
| status | Qué significa |
|---|---|
| queued | numeración asignada y CDC generado; esperando firma y envío |
| signed | XML firmado con el certificado de la empresa |
| sent | transmitido al SIFEN, sin veredicto todavía |
| approved | aprobado por el SIFEN — el documento existe legalmente |
| approved_with_observations | aprobado con observaciones; válido igual |
| rejected | rechazado; el motivo viene en mensajesSifen |
| cancelled | cancelado por un evento posterior |
| draft | borrador — no lo produce la API |
Consultar
Filtros: q (busca por CDC o por documento del receptor), status, tipoDocumento, take (1-100, default 25) y skip. El ambiente sale de la clave.
GET https://api.timbrale.com.py/v1/documentos?status=approved&take=25&skip=0
Authorization: Bearer sk_prod_...
{
"items": [
{
"id": "0f3a1c2e-...",
"cdc": "01800123450001001000000112026...",
"tipoDocumento": "factura",
"numeroCompleto": "001-001-0000001",
"status": "approved",
"fechaEmision": "2026-09-10T14:03:11.000Z",
"moneda": "PYG",
"total": "450000",
"ambiente": "prod",
"qrLink": "https://ekuatia.set.gov.py/consultas/...",
"ultimoError": null,
"ultimoIntentoAt": "2026-09-10T14:03:14.000Z",
"intentos": 1,
"receptorDoc": "80069590-9",
"receptorNombre": "EMPRESA DE PRUEBA S.A.",
"protocolo": "1234567890",
"mensajesSifen": [{ "codigo": "0260", "mensaje": "Autorizado el e-Kuatia" }]
}
],
"total": 1,
"take": 25,
"skip": 0
}Un documento, con los mismos campos de la respuesta de emisión.
El XML del DE en application/xml, firmado cuando hay certificado configurado en el ambiente.
El KuDE en PDF — el documento que recibe el receptor, el que se imprime, se adjunta o se manda. Existe recién después de que el SIFEN aprueba y el worker lo genera: hasta ahí responde 404 diciendo cuál de las dos cosas falta. El polling es el mismo que ya se hace por status.
El mismo KuDE en HTML, disponible desde el primer segundo: antes de la aprobación sale con la franja «DOCUMENTO EN PROCESO DE APROBACIÓN — SIN VALIDEZ FISCAL». Las dos rutas respetan el ambiente de la clave: una sk_test_ no baja el KuDE de un documento de producción.
Establecimientos y puntos de expedición habilitados de la empresa — los valores válidos para establecimiento y puntoExpedicion.
El ambiente de la clave, la empresa emisora, sus establecimientos y los timbrados vigentes, en una sola respuesta — lo que un programa necesita saber antes de armar el primer documento.
Webhooks
En vez de consultar cada documento, tu sistema recibe un POST cuando el SIFEN decide. Se configuran en Panel → Webhooks: URL https://, ambiente (un webhook de sandbox nunca recibe documentos de producción), eventos y, si hace falta, establecimientos. Sin establecimientos marcados llegan los de todos; una empresa con un sistema por local crea un webhook por establecimiento. El secreto de firma (whsec_…) se muestra una sola vez. Hasta 10 webhooks por ambiente, contando los pausados; para cambiar uno, se elimina y se crea otro.
| Evento | Cuándo |
|---|---|
| documento.approved | el SIFEN aprobó el documento |
| documento.approved_with_observations | aprobado con observaciones; válido igual |
| documento.rejected | rechazado; el motivo viene en datos.mensajesSifen |
| documento.cancelled | la DNIT confirmó la cancelación |
| ping | botón «Enviar prueba» del panel; datos trae solo webhookId |
datos trae los mismos campos que GET /v1/documentos/:id.
POST https://tu-sistema.com/timbrale
Content-Type: application/json
Timbrale-Evento: documento.approved
Timbrale-Entrega: cm1x8k2p40001...
Timbrale-Signature: t=1757772194,v1=5f2c9a...
{
"id": "documento.approved:0f3a1c2e-...",
"evento": "documento.approved",
"creadoEn": "2026-09-10T14:03:14.000Z",
"ambiente": "prod",
"datos": {
"id": "0f3a1c2e-...",
"cdc": "01800123450001001000000112026...",
"tipoDocumento": "factura",
"establecimiento": "001",
"puntoExpedicion": "001",
"numeroCompleto": "001-001-0000001",
"status": "approved",
"fechaEmision": "2026-09-10T14:03:11.000Z",
"moneda": "PYG",
"total": "450000",
"qrLink": "https://ekuatia.set.gov.py/consultas/...",
"protocolo": "1234567890",
"mensajesSifen": [{ "codigo": "0260", "mensaje": "Autorizado el e-Kuatia" }]
}
}Timbrale-Signature es t=<unix>,v1=<hex>: HMAC-SHA256 con el secreto sobre <t>.<cuerpo crudo>. Rechazá un t de más de 5 minutos.
import { createHmac, timingSafeEqual } from 'node:crypto';
// body: el cuerpo crudo, tal como llegó — antes de JSON.parse.
function firmaValida(body, header, secreto) {
const { t, v1 } = Object.fromEntries(header.split(',').map((p) => p.split('=')));
// Más de 5 minutos: una entrega vieja reenviada por un tercero.
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const esperada = createHmac('sha256', secreto).update(`${t}.${body}`).digest('hex');
return v1?.length === esperada.length && timingSafeEqual(Buffer.from(v1), Buffer.from(esperada));
}Respondé 2xx en menos de 10 segundos; otro código, un timeout o una redirección cuentan como falla. Reintentamos después de 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h y 24 h — 9 intentos, unos 3 días. Si todos fallan, el webhook queda detenido y avisamos por correo; al reactivarlo se entregan los eventos acumulados.
id del evento se repite en cada reintento: deduplicá por él. El orden no está garantizado — un documento.cancelled puede llegar antes que un documento.approved reintentado; usá datos.status, o GET /v1/documentos/:id ante la duda.Otros tipos de documento
Los cinco tipos comparten el mismo cuerpo; cambia lo que cada uno exige de más.
| tipoDocumento | Exige además |
|---|---|
| factura | factura.presencia y condicion |
| notaCredito | documentoAsociado (formato 1 con el CDC, o formato 2 con timbrado, establecimiento, punto, número y fecha) y notaCreditoDebito |
| notaDebito | lo mismo que la nota de crédito |
| autofactura | autoFactura con los datos del vendedor no contribuyente, cliente.tipoOperacion = 2 (B2C) y documentoAsociado formato 3 con constanciaTipo, constanciaControl y constanciaNumero |
| notaRemision | remision (motivo, tipoResponsable, kms), detalleTransporte completo y cliente.direccion; no lleva condicion |
Errores
Los errores de validación devuelven la lista de campos, nunca un código opaco. El idioma del mensaje sigue el Accept-Language.
| Código | Cuándo |
|---|---|
| 400 | falta el header Idempotency-Key en POST /v1/documentos |
| 401 | clave ausente, mal formada, revocada, o cuenta suspendida |
| 402 | el plan no incluye el recurso pedido (API en producción, dossier de fiscalización, asistente de rectificativa: recurso dice cuál y upgrade el plan que lo incluye), o el plan gratis agotó la cuota del mes (el consumo va en uso) |
| 404 | el documento no existe, o es de otro ambiente que el de la clave |
| 409 | carrera de idempotencia que no se pudo resolver; el caso normal de reenvío devuelve 200 con el documento anterior |
| 429 | más de 600 pedidos por minuto con la misma clave; Retry-After dice en cuántos segundos vuelve. Toda respuesta trae X-RateLimit-Limit, X-RateLimit-Remaining y X-RateLimit-Reset |
| 422 | validación del cuerpo (lista en fields) o regla de negocio: CSC no configurado, certificado vencido o ausente, timbrado vencido, numeración agotada o no configurada para el punto, establecimiento o punto no registrado, tipoTransaccion no configurado, fecha fuera del plazo del SIFEN (más de 720 h atrás o 120 h adelante) |
| 503 | GET /v1/documentos/:id/xml cuando la firma o el QR fallan del lado de Timbrale (certificado, contraseña, CSC) — no es culpa del payload |
422 Unprocessable Entity
{
"statusCode": 422,
"error": "Unprocessable Entity",
"fields": [
{ "path": "cliente.ruc", "message": "dígito verificador incorrecto: para 80012345 el DV es 0" },
{ "path": "items", "message": "documento precisa de pelo menos um item" }
]
}Conservación de los archivos
Timbrale conserva por 10 años, contados desde el cierre del ejercicio fiscal de cada documento, el XML firmado tal como fue transmitido al SIFEN, la respuesta del SIFEN con su protocolo, los eventos asociados (cancelación, inutilización, eventos del receptor) con sus XML firmados, y el KuDE generado. El plazo legal es la prescripción de la Ley 125/91 art. 164 — unos 6 años, suspendible—; los 10 son margen.
Nada se borra automáticamente antes de eso. De cada XML se guarda además su SHA-256 en el momento de la emisión: es lo que permite demostrar, en una fiscalización, que el archivo entregado es el original y no una copia rehecha. Un XML regenerado desde la base de datos pierde la firma digital y con ella el valor probatorio.
La exportación está siempre disponible, pero se pide desde el panel, con sesión, no con la clave de API: GET /v1/tenant/fiscal/periods/:periodo/export?format=zip devuelve los XML firmados del período, los KuDE y un manifiesto que compara cada hash con el guardado al emitir. En un período grande la respuesta es 202 con un jobId, y el archivo se retira después en GET /v1/tenant/fiscal/exports/:id/download.
Para una fiscalización que abarca varios meses está el dossier: POST /v1/tenant/fiscal/dossier con { "desde": "AAAA-MM-DD", "hasta": "AAAA-MM-DD" } responde siempre 202 con un jobId y arma los XML firmados del intervalo —también los de documentos anulados, sus eventos y las inutilizaciones—, con el control de integridad de cada uno y el estado del cierre de cada mes. Está incluido en Alto volumen y Enterprise; en los demás planes la respuesta es 402.
¿Falta algo acá?
Se crea la cuenta, se genera una clave sk_test_ y se emite contra el sandbox sin costo ni certificado.