SDK de Sifende para Go: emití facturas electrónicas, notas de crédito y eventos de SIFEN, verificá webhooks y calculá totales con las mismas reglas que la API.
- Sin dependencias: sólo la biblioteca estándar.
- Tipos generados desde el OpenAPI de la API, con los montos como
Decimalexacto: nunca pasan por unfloat. - Reintentos seguros: cada operación tributaria viaja con
Idempotency-Key.
go get github.com/ithdev/sifende-goRequiere Go 1.26 o posterior.
La referencia de cada tipo y función está en pkg.go.dev.
Necesitás una API key de Sifende. El prefijo de la clave fija el ambiente: sk_live_, sk_test_ o sk_sandbox_. La
referencia completa de la API está en sifende.com.py/docs.
La API key emite documentos a nombre del contribuyente: usá el SDK sólo en el servidor, nunca dentro de una app distribuida.
cliente, err := sifende.NuevoCliente(os.Getenv("SIFENDE_API_KEY"))
if err != nil {
return err
}
pedidoID := "1001" // el id de la venta en tu sistema
factura := &sifende.FacturaElectronicaRequest{
TipoEmision: sifende.TipoEmisionNormal,
NumeroEstablecimiento: 1,
PuntoExpedicion: 2,
TipoTransaccion: sifende.TipoTransaccionVentaMercaderia,
IndicadorPresencia: sifende.IndicadorPresenciaOperacionElectronica,
CondicionOperacion: sifende.CondicionOperacionContado,
Receptor: sifende.ReceptorDTO{
TipoContribuyente: sifende.NaturalezaNoContribuyente,
TipoOperacion: sifende.TipoOperacionB2C,
TipoDocumento: sifende.TipoDocumentoIdentidadInnominado,
NumeroDocumento: "0",
NombreRazonSocial: "Sin Nombre",
},
Items: []sifende.ItemDTO{
{
Codigo: "REM-01", Descripcion: "Remera", Cantidad: sifende.NuevoDecimal(2), UnidadMedida: "UNI",
PrecioUnitario: sifende.NuevoDecimal(150_000), AfectacionTributaria: sifende.AfectacionTributariaGravado,
TasaIVA: new(int32(10)),
},
{
Codigo: "ENVIO", Descripcion: "Envío", Cantidad: sifende.NuevoDecimal(1), UnidadMedida: "UNI",
PrecioUnitario: sifende.NuevoDecimal(25_000), AfectacionTributaria: sifende.AfectacionTributariaGravado,
TasaIVA: new(int32(10)),
},
},
}
totales, err := factura.Totales()
if err != nil {
return err
}
factura.Pagos = []sifende.PagoFacturaDTO{
{TipoPago: sifende.TipoPagoTransferencia, MontoPago: totales.TotalGeneral, MonedaPago: sifende.MonedaPYG},
}
documento, err := cliente.Documentos.Emitir(ctx, factura, "pedido-"+pedidoID)
if err != nil {
return err
}
fmt.Println(documento.CDC, documento.Estado) // PENDIENTEEl documento queda en PENDIENTE hasta que SIFEN lo procesa. El resultado llega por webhook o con
Documentos.ConsultarEstado(ctx, cdc).
En una operación al contado, MontoPago tiene que coincidir con el total redondeado. Totales (y CalcularTotales
para armar el documento a mano) aplica el redondeo oficial de la SET: en guaraníes, al múltiplo de 50 inferior.
Emitir acepta cualquier SolicitudEmision: *FacturaElectronicaRequest, *NotaCreditoElectronicaRequest,
*NotaDebitoElectronicaRequest, *NotaRemisionElectronicaRequest o *AutofacturaElectronicaRequest. Cada una envía
su tipoDocumento sola.
En una solicitud, un campo opcional que admite cero es un puntero: nil no se envía. Con Go 1.26, new(int32(10)) o
new(sifende.MustDecimal("12.5")) lo arma en una línea. Los textos y las enumeraciones opcionales no son punteros: el
valor vacío no se envía.
Los montos, cantidades y tasas son sifende.Decimal. Crealos con NuevoDecimal(150000), ParsearDecimal("19.99")
o MustDecimal("19.99") para literales. Comparar los compara sin importar la escala: 1.50 y 1.5 son iguales.
En una respuesta, los campos son valores, y los objetos anidados son punteros que quedan en nil si no vinieron.
cliente, err := sifende.NuevoCliente(apiKey,
sifende.ConTimeout(10*time.Second),
sifende.ConMaxReintentos(3),
)| Opción | Por defecto | |
|---|---|---|
ConTimeout(d) |
30 s | Tiempo máximo por intento, lectura del cuerpo incluida. |
ConMaxReintentos(n) |
2 | Reintentos ante fallas transitorias. 0 los desactiva. |
ConBaseURL(url) |
https://api.sifende.com.py |
|
ConHTTPClient(c) |
&http.Client{} |
Para usar un proxy o un transporte instrumentado. |
Todas las operaciones reciben un context.Context: cancelarlo corta la operación y sus reintentos.
| Método | Endpoint |
|---|---|
Documentos.Emitir(ctx, documento, idempotencyKey) |
POST /api/v1/documento-electronico |
Documentos.ConsultarEstado(ctx, cdc) |
GET /api/v1/documento-electronico/status/{cdc} |
Documentos.DescargarKude(ctx, cdc, espera) |
GET /api/v1/documento-electronico/{cdc}/kude |
Documentos.DescargarKudeTicket(ctx, cdc, opciones) |
GET /api/v1/documento-electronico/{cdc}/kude/ticket |
Documentos.DescargarKudeTicketImagen(ctx, cdc, opciones) |
GET /api/v1/documento-electronico/{cdc}/kude/ticket/imagen |
Documentos.DescargarXML(ctx, cdc) |
GET /api/v1/documento-electronico/{cdc}/xml |
Documentos.Cancelar(ctx, cdc, cancelacion, idempotencyKey) |
POST /api/v1/documento-electronico/{cdc}/cancelar |
Documentos.Nominar(ctx, cdc, nominacion, idempotencyKey) |
POST /api/v1/documento-electronico/{cdc}/nominar |
Documentos.Inutilizar(ctx, rango, idempotencyKey) |
POST /api/v1/documento-electronico/inutilizar |
Documentos.ListarEventos(ctx, filtro) |
GET /api/v1/documento-electronico/eventos |
Documentos.ConsultarEvento(ctx, id) |
GET /api/v1/documento-electronico/eventos/{id} |
Contribuyente.Consultar(ctx) |
GET /api/v1/contribuyente |
Contribuyente.ConsultarConsumoPlan(ctx) |
GET /api/v1/contribuyente/plan-consumo |
Padron.Consultar(ctx, rucOCedula) |
GET /api/v1/padron/{documento} |
Catalogos.Enumeraciones(ctx) |
GET /api/v1/public/enums |
Catalogos.Departamentos(ctx), Distritos(ctx, id), Ciudades(ctx, id), CiudadesDeDepartamento(ctx, id) |
GET /api/v1/geografia/… |
Webhooks.Listar(ctx), Crear(ctx, webhook) |
GET, POST /api/v1/contribuyente/webhooks |
Webhooks.Consultar(ctx, id), Actualizar(ctx, id, webhook), Eliminar(ctx, id) |
GET, PUT, DELETE /api/v1/contribuyente/webhooks/{id} |
Webhooks.RotarSecreto(ctx, id) |
POST /api/v1/contribuyente/webhooks/{id}/rotar |
Webhooks.ListarEntregas(ctx, id, filtro) |
GET /api/v1/contribuyente/webhooks/{id}/entregas |
Los catálogos son públicos y no envían la API key.
DescargarKude espera hasta 3 minutos a que el PDF esté listo; si no lo está, devuelve *ErrorKudePendiente. Con
espera distinta de cero cambiás ese plazo.
DescargarKudeTicket devuelve el KuDE en cinta de papel para impresoras térmicas (Ancho 80 o 58 mm) y
DescargarKudeTicketImagen el mismo ticket como PNG de 1 bit para ESC/POS (Puntos 576, 512 o 384). Con
Resumen: true omiten el detalle de ítems e impuestos. Sólo existen para facturas y notas de crédito o débito.
DescargarXML devuelve el XML firmado tal como se envió a SIFEN.
Para poder retomar una operación después de un corte, generá vos la idempotencyKey y guardala antes de enviar. Si
pasás "", el SDK genera un UUID que sólo reusa en sus propios reintentos.
_, err := cliente.Documentos.Emitir(ctx, factura, idempotencyKey)
var errorAPI *sifende.ErrorAPI
var errorConexion *sifende.ErrorConexion
switch {
case errors.As(err, &errorAPI):
errorAPI.Status // 400
errorAPI.Codigo // "validation-error", "document-quota-exceeded", …
errorAPI.Errores() // map[receptor.digitoVerificador:…]
case errors.As(err, &errorConexion):
errorConexion.IdempotencyKey // repetí la operación con esta clave
errorConexion.Timeout // true si venció el timeout, incluso a mitad de la respuesta
}Si cancelás el context, el error es el del contexto (context.Canceled o la causa que pasaste a
context.WithCancelCause), no un error del SDK.
El SDK reintenta sólo los casos que la API marca como seguros, y respeta Retry-After:
- Cortes de conexión en consultas y en operaciones con clave de idempotencia.
409 idempotency-in-progress,503 idempotency-upstream-unknowny503 evento-nominacion-error.429.500,502,503y504en consultas.
La firma se calcula sobre el cuerpo tal como llegó. Pasá los bytes crudos, antes de decodificar el JSON:
http.HandleFunc("POST /webhooks/sifende", func(w http.ResponseWriter, r *http.Request) {
cuerpo, err := io.ReadAll(r.Body)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
evento, err := sifende.VerificarWebhook(sifende.OpcionesVerificacion{
Cuerpo: cuerpo,
Headers: r.Header,
Secretos: []string{os.Getenv("SIFENDE_WEBHOOK_SECRET")},
})
if errors.Is(err, sifende.ErrFirmaWebhookInvalida) {
w.WriteHeader(http.StatusUnauthorized)
return
}
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
if evento.Tipo == sifende.TipoEventoWebhookDocumentoAprobado {
if datos, err := evento.Documento(); err == nil {
log.Println(datos.CDC, datos.Estado)
}
}
w.WriteHeader(http.StatusNoContent)
})Los tipos de evento son documento.aprobado, documento.rechazado, documento.cancelado y lote.procesado. Su
Data se lee con evento.Documento(), evento.Cancelacion() o evento.Lote().
Webhooks registra y administra los endpoints. El secreto whsec_… llega sólo en la respuesta de Crear y de
RotarSecreto, y ninguno de los dos admite Idempotency-Key: el SDK no los repite ante un corte. Si se corta un
Crear, revisá Listar antes de volver a intentar.
Una entrega puede repetirse: deduplicá por evento.ID. VerificarWebhook rechaza una entrega si su
X-Sifende-Timestamp difiere más de 5 minutos de tu reloj; ajustalo con Tolerancia. Durante una rotación de secreto,
pasá Secretos: []string{nuevo, anterior}. FirmarWebhook calcula la firma para probar tu endpoint.
go vet ./... && go test -race ./...tipos_gen.go se genera desde el OpenAPI de la API con internal/generar, que sólo usa la biblioteca estándar. Para
regenerarlo contra producción o contra una copia local del spec:
go generate ./...
SIFENDE_OPENAPI=./openapi.json go generate ./...- Errores del SDK: abrí un issue.
- Vulnerabilidades: reportalas en privado, como explica SECURITY.md.
- Tu cuenta, tus documentos o la API: escribinos a soporte@sifende.com.py.
Apache 2.0. Copyright 2026 ITH E.A.S.; ver NOTICE.