Skip to content

About

SDK oficial de fiscalapi para consumo desde aplicaciones Java

Resources

Stars

63 stars

Watchers

0 watching

Forks

Repository files navigation

Fiscalapi SDK para Java

Maven Central License

SDK oficial de https://fiscalapi.com para Java, la API de facturación CFDI y otros servicios fiscales en México. Simplifica la integración con los servicios de facturación electrónica, eliminando las complejidades del SAT y facilitando la generación de facturas, notas de crédito, complementos de pago, nómina, carta porte, y más. ¡Factura sin dolor!

📋 Facturación CFDI 4.0

  • Soporte completo para CFDI 4.0 con todas las especificaciones oficiales
  • Timbrado de facturas de ingreso con validación automática
  • Timbrado de notas de crédito (facturas de egreso)
  • Timbrado de complementos de pago en MXN, USD y EUR
  • Timbrado de facturas de nómina
  • Timbrado de facturas de carta porte
  • Timbrado de facturas de comercio exterior
  • Consulta del estatus de facturas en el SAT en tiempo real
  • Cancelación de facturas
  • Generación de archivos PDF de las facturas con formato profesional
  • Personalización de logos y colores en los PDF generados
  • Envío de facturas por correo electrónico automatizado
  • Descarga de archivos XML con estructura completa
  • Almacenamiento y recuperación de facturas por 5 años.
  • Dos modos de operación: Por valores o Por referencias

📥 Descarga Masiva

  • Acceso a catálogos de descarga masiva del SAT
  • Descarga de CFDI y Metadatos en lotes grandes
  • Descarga masiva XML con filtros personalizados
  • Reglas de descarga automática por RFC
  • Solicitudes de descarga via API y Dashboard.
  • Automatización de solicitudes de descarga

👥 Gestión de Personas

  • Administración de personas (emisores, receptores, clientes, usuarios, etc.)
  • Gestión de certificados CSD y FIEL (subir archivos .cer y .key a FiscalAPI)
  • Configuración de datos fiscales (RFC, domicilio fiscal, régimen fiscal)
  • Datos de empleado (agrega/actualiza/elimina datos de empleado a una persona. CFDI Nómina)
  • Datos de empleador (agrega/actualiza/elimina datos de empleador a una persona. CFDI Nómina)
  • Firma de carta manifiesto (firma el manifiesto con la FIEL de la persona y obtén el PDF)

🎖️ Gestión de Timbres

  • Gestión de folios fiscales Compra timbres a fiscalapi y transfiere/retira a las personas de tu organizacion segun tus reglas de negocio.
  • Listar transacciones de timbres con paginación
  • Consultar transacciones por ID
  • Transferir timbres entre personas
  • Retirar timbres de una persona
  • Créditos de validación SAT transfiere y retira créditos de validación con el mismo servicio, eligiendo el saldo con creditType
  • Créditos de ticket transfiere y retira créditos de ticket con CreditType.TICKET; su saldo se lee en balances de la persona

✅ Validaciones SAT

  • Validación de CFDI timbrados estructura del Anexo 20, vigencia del certificado del emisor, sello del CFDI y sello del SAT en el TimbreFiscalDigital
  • Estatus del comprobante en el SAT vigente, cancelado o no encontrado
  • Listas negras del SAT artículos 69-B y 69-B Bis del CFF, por CFDI o por RFC
  • Catálogo de validaciones consulta los tipos disponibles y los estatus que cada uno puede tomar

🧾 Tickets (Auto CFDI)

  • Ingesta de ventas pagadas del punto de venta con Idempotency-Key opcional (getTicketService().create)
  • Consulta con filtros por estado, emisor, rango de fechas de pago y página; detalle con conceptos e historia
  • Código para reimprimir y cancelación de tickets pendientes (ejemplo en examples/EjemplosTickets.java)

🛍️ Gestión de Productos/Servicios

  • Gestión de productos y servicios con catálogo personalizable
  • Administración de impuestos aplicables (IVA, ISR, IEPS)

📚 Consulta de Catálogos SAT

  • Consulta en catálogos oficiales del SAT actualizados
  • Consulta en catálogos oficiales de Descarga masiva del SAT actualizados
  • Búsqueda de información en catálogos del SAT con filtros avanzados
  • Acceso y búsqueda en catálogos completos

📖 Recursos Adicionales

  • Cientos de ejemplos de código disponibles en múltiples lenguajes de programación
  • Documentación completa con guías paso a paso
  • Ejemplos prácticos para casos de uso comunes
  • Soporte técnico especializado
  • Actualizaciones regulares conforme a cambios del SAT

📦 Instalación

Compatible con múltiples versiones de Java (desde Java 8 en adelante)

Maven:

<dependency>
    <groupId>com.fiscalapi</groupId>
    <artifactId>fiscalapi</artifactId>
    <version>4.0.450</version>
</dependency>

Gradle (Groovy):

implementation 'com.fiscalapi:fiscalapi:4.0.450'

Gradle (Kotlin):

implementation("com.fiscalapi:fiscalapi:4.0.450")

Para más información, consulta Snippets en Maven Central.

⚙️ Configuración

Puedes usar el SDK tanto en aplicaciones sin inyección de dependencias como en proyectos que usan Spring Boot. A continuación se describen ambas formas:

A) Aplicaciones sin Inyección de Dependencias

  1. Crea tu objeto de configuración con tus credenciales:
FiscalApiSettings settings = new FiscalApiSettings();
settings.setDebugMode(true);
settings.setApiUrl("https://test.fiscalapi.com"); // https://live.fiscalapi.com (producción) 
settings.setApiKey("<api-key>");
settings.setTenant("<tenant-key>");
FiscalApiClient client = FiscalApiClient.create(settings);
  1. Utiliza los servicios de FiscalApiClient según tus necesidades (ver ejemplos más adelante).

B) Aplicaciones con Spring Boot

  1. Agrega la configuración en tu application.properties o application.yml:
fiscalapi:
  api-url: https://test.fiscalapi.com
  api-key: <YourApiKeyHere>
  tenant: <YourTenantHere>
  1. Registra el bean en tu configuración de Spring:
@Configuration
public class FiscalApiConfig {
    @Bean
    public FiscalApiClient fiscalApiClient(
        @Value("${fiscalapi.api-url}") String apiUrl,
        @Value("${fiscalapi.api-key}") String apiKey,
        @Value("${fiscalapi.tenant}") String tenant
    ) {
        FiscalApiSettings settings = new FiscalApiSettings();
        settings.setApiUrl(apiUrl);
        settings.setApiKey(apiKey);
        settings.setTenant(tenant);
        return FiscalApiClient.create(settings);
    }
}

Posteriormente, podrás inyectar FiscalApiClient donde lo requieras:

@RestController
public class InvoicesController {
    private final FiscalApiClient fiscalApi;

    public InvoicesController(FiscalApiClient fiscalApi) {
        this.fiscalApi = fiscalApi;
    }
    
    // Usa fiscalApi en tus métodos del controlador...
}

📝 Ejemplos de Uso

1. Crear una Persona

// ***Crear persona ***//
Person person = new Person();
person.setLegalName("MI EMPRESA"); // razón social sin régimen de capital
person.setEmail("john.doe@fiscalapi.com");
person.setPassword("Password123!");
ApiResponse<Person> apiResponse = client.getPersonService().create(person);
System.out.printf("apiResponse: %s\n", apiResponse);

2. Subir Certificados CSD

// ***Subir archivo .cer (certificado)***//
TaxFile archivoCer = new TaxFile();
archivoCer.setPersonId("3f3478b4-60fd-459e-8bfc-f8239fc96257");
archivoCer.setTin("FUNK671228PH6"); // RFC de la persona (opcional)
archivoCer.setBase64File("MIIFgDCCA2igAwIBAgIUMzAwMDEwMDAwMDA1MDAwMDM0NDYwDQYJKoZIhvcNAQELBQAwggEr...");
archivoCer.setFileType(0); // 0 para certificado
archivoCer.setPassword("12345678a");

ApiResponse<TaxFile> apiResponseCer = client.getTaxFileService().create(archivoCer);
System.out.printf("apiResponse: %s\n", apiResponseCer);


// ***Subir archivo .key (clave privada)***//
TaxFile archivoKey = new TaxFile();
archivoKey.setPersonId("3f3478b4-60fd-459e-8bfc-f8239fc96257");
archivoKey.setTin("FUNK671228PH6"); // RFC de la persona (opcional)
archivoKey.setBase64File("MIIFDjBABgkqhkiG9w0BBQ0...==");
archivoKey.setFileType(1); // 1 para llave privada
archivoKey.setPassword("12345678a");

ApiResponse<TaxFile> apiResponseKey = client.getTaxFileService().create(archivoKey);
System.out.printf("apiResponse: %s\n", apiResponseKey);

3. Crear un Producto o Servicio

// ***Crear producto***//
Product product = new Product();
product.setDescription("Libro de Java sin impuestos");
product.setUnitPrice(100.75986);
ApiResponse<Product> apiResponseProduct = client.getProductService().create(product);
System.out.printf("apiResponse: %s\n", apiResponseProduct);

4. Crear una Factura de Ingreso (Por Referencias)

// Generar factura por referencias
Invoice invoice = new Invoice();
invoice.setVersionCode("4.0");
invoice.setSeries("F");
invoice.setDate(LocalDateTime.now());
invoice.setPaymentFormCode("01");
invoice.setCurrencyCode("MXN");
invoice.setTypeCode("I");
invoice.setExpeditionZipCode("42501");
invoice.setPaymentMethodCode("PUE");

// Emisor por referencia
InvoiceIssuer issuer = new InvoiceIssuer();
issuer.setId("3f3478b4-60fd-459e-8bfc-f8239fc96257");
invoice.setIssuer(issuer);

// Receptor por referencia
InvoiceRecipient recipient = new InvoiceRecipient();
recipient.setId("96b46762-d246-4a67-a562-510a25dbafa9");
invoice.setRecipient(recipient);

// Producto / Servicio por referencia
List<InvoiceItem> items = new ArrayList<>();
InvoiceItem item = new InvoiceItem();
item.setId("114a4be5-fb65-40b2-a762-ff0c55c6ebfa");
item.setQuantity(2.0);
item.setDiscount(255.85);
items.add(item);
invoice.setItems(items);

ApiResponse<Invoice> apiResponseInvoice = client.getInvoiceService().create(invoice);
System.out.println(apiResponseInvoice);

5. Validar un CFDI ante el SAT

Cada tipo de validación solicitado consume un crédito de validación. El cobro es todo o nada: si el saldo no alcanza para todos, no se ejecuta ninguno. Se envía xml (CFDI timbrado en base64) o tin (RFC), nunca ambos; con tin solo se pueden solicitar listas negras.

// *** Validar un CFDI timbrado ***//
SatValidationRequest request = new SatValidationRequest();
request.setXml(Base64.getEncoder().encodeToString(Files.readAllBytes(Paths.get("C:\\facturas\\FacturaXml.xml"))));
request.setValidationTypes(new ArrayList<>(Arrays.asList(
        SatValidationTypeIds.XML_STRUCTURE,
        SatValidationTypeIds.CFDI_SELLO,
        SatValidationTypeIds.CFDI_STATUS,
        SatValidationTypeIds.BLACKLIST_69B)));

ApiResponse<List<SatValidationResult>> apiResponse = client.getSatValidationService().validate(request);

for (SatValidationResult result : apiResponse.getData()) {
    System.out.printf("%s -> %s (passed: %s)%n",
            result.getType().getId(), result.getStatus().getId(), result.isPassed());
}

6. Transferir créditos de validación o de ticket

El mismo servicio de timbres mueve los saldos, que nunca se mezclan: CreditType.STAMP (por defecto) mueve timbres, CreditType.VALIDATION mueve créditos de validación SAT y CreditType.TICKET mueve créditos de ticket.

// *** Transferir créditos de validación ***//
StampTransactionParams transParams = new StampTransactionParams();
transParams.setFromPersonId("0e82a655-5f0c-4e07-abab-8f322e4123ef");
transParams.setToPersonId("da71df0c-f328-45ee-9bd9-3096ed02c164");
transParams.setAmount(10);
transParams.setComments("venta de creditos de validacion");
transParams.setCreditType(CreditType.VALIDATION);

ApiResponse<Boolean> apiResponse = client.getStampService().transferStamps(transParams);
System.out.printf("apiResponse: %s\n", apiResponse);

// *** Transferir créditos de ticket ***//
StampTransactionParams ticketParams = new StampTransactionParams();
ticketParams.setFromPersonId("0e82a655-5f0c-4e07-abab-8f322e4123ef");
ticketParams.setToPersonId("da71df0c-f328-45ee-9bd9-3096ed02c164");
ticketParams.setAmount(100);
ticketParams.setComments("venta de creditos de ticket");
ticketParams.setCreditType(CreditType.TICKET);

ApiResponse<Boolean> ticketResponse = client.getStampService().transferStamps(ticketParams);
System.out.printf("ticketResponse: %s\n", ticketResponse);

// El saldo de créditos de ticket se lee en balances de la persona (un tipo ausente tiene saldo 0)
Person person = client.getPersonService().getById("da71df0c-f328-45ee-9bd9-3096ed02c164", false).getData();
int creditosDeTicket = person.getBalances().stream()
        .filter(balance -> balance.getCreditType() == CreditType.TICKET.getValue())
        .mapToInt(CreditBalance::getAvailable)
        .sum();

7. Factura con Complemento de Comercio Exterior (Por Valores)

El complemento se cuelga de Complement.setComercioExterior(...). El TotalUSD no se envía: la API lo calcula sumando el valorDolares de las mercancías.

⚠️ La escala decimal importa. Construye los importes con new BigDecimal("...") a partir de una cadena, nunca desde un double. El SAT valida el número de decimales: 120 en lugar de 120.00 se rechaza con CCE122, y 0.16 en lugar de 0.160000 con CFDI40179.

InvoiceRecipient recipient = new InvoiceRecipient();
recipient.setTin("XEXX010101000");
recipient.setLegalName("Persona Fisica Extranjera");
recipient.setZipCode("42501");
recipient.setTaxRegimeCode("616");
recipient.setCfdiUseCode("S01");
recipient.setCountryId("USA");        // Residencia fiscal (c_Pais)
recipient.setForeignTin("123456789"); // NumRegIdTrib

ComercioExteriorEmisorDomicilio domicilioEmisor = new ComercioExteriorEmisorDomicilio();
domicilioEmisor.setCalle("CALLE DEL PAPEL");
domicilioEmisor.setColoniaId("0214");
domicilioEmisor.setLocalidadId("01");
domicilioEmisor.setMunicipioId("014");
domicilioEmisor.setEstadoId("QUE");
domicilioEmisor.setPaisId("MEX");
domicilioEmisor.setCodigoPostalId("76199");

ComercioExteriorEmisor emisor = new ComercioExteriorEmisor();
emisor.setDomicilio(domicilioEmisor);

// El domicilio del receptor es extranjero: va en texto libre, sin sufijo Id
ComercioExteriorReceptorDomicilio domicilioReceptor = new ComercioExteriorReceptorDomicilio();
domicilioReceptor.setCalle("ST. A");
domicilioReceptor.setEstado("TX");
domicilioReceptor.setPaisId("USA");
domicilioReceptor.setCodigoPostal("00000");

ComercioExteriorReceptor receptor = new ComercioExteriorReceptor();
receptor.setNumRegIdTrib("123456789");
receptor.setDomicilio(domicilioReceptor);

ComercioExteriorMercancia mercancia = new ComercioExteriorMercancia();
mercancia.setNoIdentificacion("131494-1055");
mercancia.setFraccionArancelariaId("2402200100");
mercancia.setCantidadAduana(new BigDecimal("2.00"));
mercancia.setUnidadAduanaId("01");
mercancia.setValorUnitarioAduana(new BigDecimal("11.74"));
mercancia.setValorDolares(new BigDecimal("23.47"));

List<ComercioExteriorMercancia> mercancias = new ArrayList<ComercioExteriorMercancia>();
mercancias.add(mercancia);

ComercioExterior comercioExterior = new ComercioExterior();
comercioExterior.setClaveDePedimentoId("A1");
comercioExterior.setCertificadoOrigen(0);
comercioExterior.setIncotermId("FOB");
comercioExterior.setTipoCambioUSD(new BigDecimal("17.4948")); // tipo de cambio DOF de la fecha
comercioExterior.setEmisor(emisor);
comercioExterior.setReceptor(receptor);
comercioExterior.setMercancias(mercancias);

Complement complement = new Complement();
complement.setComercioExterior(comercioExterior);

invoice.setRecipient(recipient);
invoice.setComplement(complement);

ApiResponse<Invoice> apiResponse = client.getInvoiceService().create(invoice);

En modo por referencias basta con recipient.setId(...), siempre que la persona tenga capturados countryId y foreignTin.

Los conceptos también pueden ir por referencia: un InvoiceItem con sólo id y quantity toma del producto la clave del SAT, la unidad, la descripción, el precio y los impuestos. En ese caso el NoIdentificacion del CFDI queda con el id del producto, así que mercancias[].noIdentificacion debe llevar ese mismo id:

InvoiceItem item = new InvoiceItem();
item.setId(PRODUCTO_CIGARROS_ID);
item.setQuantity(new BigDecimal("2"));

mercancia.setNoIdentificacion(PRODUCTO_CIGARROS_ID);

Un producto siempre tiene precio mayor que cero, así que los traslados con valor unitario 0 envían sus conceptos en línea.


8. Firmar Carta Manifiesto

Requiere la FIEL (e.firma), no el CSD de timbrado, y que el RFC del certificado corresponda a una persona del tenant.

SignManifestRequest request = new SignManifestRequest();
request.setBase64Cer("MIIGBDCCA+ygAwIBAgIUMzAwMDEwMDAwMDA1MDAwMDM0MTU..."); // .cer de la FIEL
request.setBase64Key("MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQI..."); // .key de la FIEL
request.setPassword("12345678a");

ApiResponse<FileResponse> apiResponse = client.getManifestService().sign(request);

if (apiResponse.isSucceeded()) {
    FileResponse manifiesto = apiResponse.getData();
    byte[] pdf = Base64.getDecoder().decode(manifiesto.getBase64File());
    Files.write(Paths.get("C:\\facturas", manifiesto.getFileName()), pdf);
}

Al firmar, todas las personas del tenant con el RFC del certificado quedan con manifestStatusId en Signed.


9. Recibir webhooks

FiscalAPI firma cada entrega con Standard Webhooks y el secreto whsec_… de su webhook. Webhook.verify (paquete com.fiscalapi.webhooks) comprueba la firma (en tiempo constante) y que webhook-timestamp no difiera más de 5 minutos del reloj del receptor, y devuelve el WebhookEvent. La firma es la única prueba de que la entrega viene de FiscalAPI.

Verifique el cuerpo crudo: páselo tal como llegó, como byte[] (@RequestBody byte[] en Spring, que no lo reserializa) o como el texto UTF-8 de esos bytes. Con byte[], Webhook.verify lo lee como UTF-8 estricto y rechaza bytes inválidos. Un cuerpo deserializado y vuelto a serializar (por ejemplo, con @RequestBody WebhookEvent o @RequestBody Map) cambia sus bytes y la firma deja de coincidir.

import com.fiscalapi.models.webhooks.WebhookEvent;
import com.fiscalapi.models.webhooks.WebhookEventTypes;
import com.fiscalapi.services.FiscalApiClient;
import com.fiscalapi.webhooks.Webhook;
import com.fiscalapi.webhooks.WebhookVerificationException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.task.TaskExecutor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
public class FiscalApiWebhooksController {
    private final TaskExecutor taskExecutor;
    private final FiscalApiClient fiscalApi;
    private final String webhookSecret; // whsec_… del webhook, desde su configuración segura

    public FiscalApiWebhooksController(TaskExecutor taskExecutor, FiscalApiClient fiscalApi,
                                       @Value("${fiscalapi.webhook-secret}") String webhookSecret) {
        this.taskExecutor = taskExecutor;
        this.fiscalApi = fiscalApi;
        this.webhookSecret = webhookSecret;
    }

    @PostMapping("/webhooks/fiscalapi")
    public ResponseEntity<Void> receive(@RequestHeader Map<String, String> headers, @RequestBody byte[] rawBody) {
        WebhookEvent event;
        try {
            event = Webhook.verify(headers, rawBody, webhookSecret); // los bytes tal como llegaron
        } catch (WebhookVerificationException e) {
            return ResponseEntity.badRequest().build(); // firma, header, timestamp o cuerpo UTF-8 inválido
        }

        // Responda 2xx rápido y procese después: FiscalAPI corta cada intento a los 15 s y lo reintenta.
        // El executor en memoria es ilustrativo: en producción use una cola durable.
        taskExecutor.execute(() -> process(event));
        return ResponseEntity.ok().build();
    }

    private void process(WebhookEvent event) {
        // La entrega es al menos una vez: event.getId() (el header webhook-id) se repite en cada reintento; descarte
        // los ya procesados. El orden de llegada no está garantizado: el estado autoritativo se lee del recurso.
        String resourceId = event.getData().getRelatedObject().getId();
        switch (event.getType()) {
            case WebhookEventTypes.INVOICE_CANCELED:
                fiscalApi.getInvoiceService().getById(resourceId, false);
                break;
            case WebhookEventTypes.DOWNLOAD_REQUEST_COMPLETED:
                fiscalApi.getDownloadRequestService().getById(resourceId, false);
                break;
            default:
                break; // un tipo que no conoce se ignora: el catálogo puede crecer
        }
    }
}
  • headers acepta Map<String, String> (como @RequestHeader Map<String, String> de Spring) o Map<String, List<String>> (un MultiValueMap de Spring o un mapa armado con HttpServletRequest#getHeaders); los nombres se comparan sin distinguir mayúsculas. Desde Spring 7, HttpHeaders ya no es un Map: pase headers.toSingleValueMap().
  • WebhookEvent es el mismo sobre para todo evento: getId(), getType() (texto; compárelo con las constantes de WebhookEventTypes), getCreatedAt() (OffsetDateTime en UTC), getPersonId() (null si el recurso no tiene persona) y getData() con getRelatedObject() (getId(), getType(), getUrl()), getStatus() y getPreviousStatus().
  • getRelatedObject().getUrl() es la ruta relativa del GET v4 del recurso (/api/v4/invoices/{id}); para leerlo, use el servicio de su tipo con getRelatedObject().getId().
  • El botón "Enviar prueba" del webhook (POST /api/v4/webhooks/{id}/test) envía WebhookEventTypes.WEBHOOK_TEST (webhook.test), cuyo relatedObject es el webhook mismo (type webhook, url /api/v4/webhooks/{id}), sin persona ni estados; no se suscribe.
  • Durante una rotación del secreto, FiscalAPI firma con el nuevo y con el anterior por 24 h: pase ambos, Webhook.verify(headers, body, Arrays.asList(secretoNuevo, secretoAnterior)).
  • La tolerancia es configurable (Webhook.verify(headers, body, secreto, Duration.ofMinutes(2))); el valor por omisión es Webhook.DEFAULT_TOLERANCE.
  • WebhookVerificationException (no verificada: extiende RuntimeException) indica una entrega que no es auténtica. Un secreto mal copiado (no es base64 canónico, por ejemplo truncado o con espacios), una lista de secretos vacía, una tolerancia negativa o un argumento null lanzan IllegalArgumentException, con la posición del secreto en la lista (nunca su valor): es un error de configuración, no de la entrega.

💡 Tip: Para más ejemplos (facturas por valores, notas de crédito, complementos de pago, catálogos SAT, cancelaciones, etc.), revisa la documentación oficial en docs.fiscalapi.com.

📄 Licencia

Este proyecto está licenciado bajo la Licencia Apache 2.0. Consulta el archivo LICENSE para más detalles.

🔗 Enlaces Útiles


Desarrollado con ❤️ por Fiscalapi

About

SDK oficial de fiscalapi para consumo desde aplicaciones Java

Resources

Stars

63 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages