Integración de VeriFactu con Java
Si estás desarrollando un ERP, un SaaS de facturación o cualquier software que necesite cumplir con la normativa VeriFactu, esta guía te muestra el camino más corto desde cero hasta emitir tu primera factura verificada con Java.
VeriFactuAPI es un servicio SaaS que abstrae toda la complejidad del protocolo de la AEAT: la generación del XML firmado, la cadena de huellas, el envío y la gestión de respuestas. Tú llamas a una API REST y nosotros nos encargamos del resto. No necesitas certificados propios, ni lidiar con los XSD de Hacienda, ni mantener la infraestructura de envío.
Esta guía usa java.net.http.HttpClient y javax.json, ambos disponibles en el JDK desde Java 11 sin ninguna instalación adicional. La API es REST pura y puedes integrarla con las clases estándar del JDK.
Requisitos e instalación
No necesitas instalar librerías adicionales para esta integración. Solo necesitas:
- Java 11 o superior
- Una cuenta en verifactuapi.es (el plan gratuito incluye entorno de pruebas)
- Clases nativas del JDK:
HttpClient,HttpRequest,HttpResponse,javax.jsony la cabeceraAuthorization: Bearer <token>
No existe un SDK oficial específico para Java: la integración se hace directamente contra la API REST, lo cual significa cero dependencias propietarias y total control sobre tu código.
Flujo completo: de la autenticación a la factura registrada
El flujo para enviar una factura tiene tres pasos: autenticarte, generar el registro de facturación y comprobar su estado en la AEAT. Esta comprobación puede hacerse mediante webhook (recomendado) o con consulta manual (polling).
Paso 1 — Autenticación y obtención del token
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import javax.json.Json;
import javax.json.JsonObject;
import javax.json.JsonReader;
import java.io.StringReader;
public class VerifactuClient {
private static final String BASE_URL = "https://app.verifactuapi.es/api";
private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(30))
.build();
public static String login(String email, String password) throws Exception {
String payload = Json.createObjectBuilder()
.add("email", email)
.add("password", password)
.build()
.toString();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/login"))
.timeout(Duration.ofSeconds(30))
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
HttpResponse<String> response;
try {
response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
} catch (Exception ex) {
throw new RuntimeException("Error de red en login: " + ex.getMessage(), ex);
}
JsonObject data;
try (JsonReader reader = Json.createReader(new StringReader(response.body()))) {
data = reader.readObject();
} catch (Exception ex) {
throw new RuntimeException("Respuesta JSON inválida en login.", ex);
}
if (!data.getBoolean("success", false)) {
throw new RuntimeException("Login fallido: "
+ data.getInt("code", 0) + ", " + data.getString("message", ""));
}
return data.getString("token");
}
}Ejemplo de uso
String token = VerifactuClient.login("tu@email.com", "tu_password");Paso 2 — Generar registro de facturación
Este es el núcleo de la integración. El siguiente ejemplo cubre el caso más habitual: una factura ordinaria (F1) con IVA al 21%.
import javax.json.JsonArray;
public static JsonObject registrarFactura(String token, String facturaJson) throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/alta-registro-facturacion"))
.timeout(Duration.ofSeconds(30))
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.header("Authorization", "Bearer " + token)
.POST(HttpRequest.BodyPublishers.ofString(facturaJson))
.build();
HttpResponse<String> response;
try {
response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
} catch (Exception ex) {
throw new RuntimeException("Error de red al registrar factura: " + ex.getMessage(), ex);
}
JsonObject data;
try (JsonReader reader = Json.createReader(new StringReader(response.body()))) {
data = reader.readObject();
} catch (Exception ex) {
throw new RuntimeException("Respuesta JSON inválida al registrar factura.", ex);
}
if (!data.getBoolean("success", false)) {
throw new RuntimeException("Error al registrar factura: "
+ data.getString("message", ""));
}
JsonArray items = data.getJsonObject("data").getJsonArray("items");
return items.getJsonObject(0);
}Ejemplo de uso
String datosFactura = Json.createObjectBuilder()
.add("IDEmisorFactura", "A39200019")
.add("NumSerieFactura", "FAC/2025/0001")
.add("FechaExpedicionFactura", "2025-04-22")
.add("TipoFactura", "F1")
.add("DescripcionOperacion", "Servicios de desarrollo web - Abril 2025")
.addNull("EmitidaPorTercODesti")
.add("Destinatarios", Json.createArrayBuilder()
.add(Json.createObjectBuilder()
.add("NombreRazon", "Cliente Ejemplo S.A.")
.add("NIF", "39707287H")))
.add("Desglose", Json.createArrayBuilder()
.add(Json.createObjectBuilder()
.add("Impuesto", 1)
.add("ClaveRegimen", 1)
.add("CalificacionOperacion", 1)
.add("TipoImpositivo", 21)
.add("BaseImponibleOImporteNoSujeto", 1000)
.add("CuotaRepercutida", 210)))
.add("CuotaTotal", 210)
.add("ImporteTotal", 1210)
.build()
.toString();
JsonObject factura = VerifactuClient.registrarFactura(token, datosFactura);
int registroId = factura.getInt("id");
String qrImageB64 = factura.getString("qr_image");
String urlQr = factura.containsKey("url_qr") ? factura.getString("url_qr") : null;
String estadoAeat = factura.getString("estado_aeat");
System.out.println("Factura registrada con ID: " + registroId);
System.out.println("Estado inicial: " + estadoAeat);
System.out.println("URL validación AEAT: " + (urlQr != null ? urlQr : "No disponible"));Paso 3 — Consultar el estado en la AEAT
El envío a la AEAT es asíncrono. La factura se registra inmediatamente en nuestra plataforma con estado "No Registrado" y se envía en el siguiente ciclo de proceso. Puedes consultar el estado de dos formas:
Opción A — Polling manual
import java.util.Map;
import java.util.HashMap;
public static Map<String, String> consultarEstado(String token, int registroId) throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/alta-registro-facturacion/" + registroId))
.timeout(Duration.ofSeconds(30))
.header("Accept", "application/json")
.header("Authorization", "Bearer " + token)
.GET()
.build();
HttpResponse<String> response;
try {
response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
} catch (Exception ex) {
throw new RuntimeException("Error de red al consultar estado: " + ex.getMessage(), ex);
}
JsonObject data;
try (JsonReader reader = Json.createReader(new StringReader(response.body()))) {
data = reader.readObject();
} catch (Exception ex) {
throw new RuntimeException("Respuesta JSON inválida al consultar estado.", ex);
}
if (!data.getBoolean("success", false)) {
throw new RuntimeException("Error al consultar el estado de la factura: "
+ data.getString("message", ""));
}
JsonObject items = data.getJsonObject("data").getJsonObject("items");
Map<String, String> resultado = new HashMap<>();
resultado.put("estado_aeat", items.getString("estado_aeat", ""));
resultado.put("codigo_error_aeat", items.getString("codigo_error_aeat", ""));
resultado.put("descripcion_error_aeat", items.getString("descripcion_error_aeat", ""));
return resultado;
}Ejemplo de uso
Map<String, String> estado = VerifactuClient.consultarEstado(token, registroId);
System.out.println("Estado AEAT: " + estado.get("estado_aeat"));
// Valores posibles: "No Registrado" | "Pendiente" | "Incorrecto" | "Correcto"Opción B — Webhook (recomendado)
Configura un endpoint en tu aplicación y registra la URL en el panel de VeriFactuAPI. Recibirás una notificación en cuanto la AEAT responda, sin necesidad de hacer polling:
{
"estado": "Correcto",
"num_serie": "FAC/2025/0001",
"factura_id": 1,
"codigo_error": "",
"descripcion_error": "",
"fecha_notificacion": "YYYY-MM-DD HH:MM:SS"
}Formatos de respuesta JSON
Las respuestas de la API siguen una estructura JSON consistente. Entender estos formatos te permite validar respuestas de forma uniforme con javax.json.
Formato 1 — success (operación correcta)
{
"success": true,
"message": "Inicio de sesión exitoso",
"code": 200,
"token": "eyJ...",
"expires_at": "2026-04-24 18:30:00"
}success:truecuando la operación finaliza correctamente.message: texto descriptivo del resultado.code: código HTTP devuelto por la API.
Campos opcionales: token, expires_at, user_name, user_email, api_key, etc.
Formato 2 — fail (error)
{
"success": false,
"message": "El campo IDEmisorFactura es obligatorio.",
"error": "VALIDATION_ERROR",
"code": 400
}success:falsecuando la operación falla.message: descripción del error para mostrar o registrar en logs.error: código interno o técnico del error.code: código HTTP asociado al fallo.
Formato 3 — list (listados)
{
"success": true,
"message": "Invoice listed successfully",
"code": 200,
"data": {
"items": [],
"count": 0
},
"pagination": {
"total": 0,
"perPage": 0,
"currentPage": 0,
"lastPage": 0
}
}data.items: array con resultados.data.count: número de elementos devueltos en la página actual.pagination: bloque con metadatos de paginación.
Próximos pasos
- Consulta la referencia completa de la API para ver todos los parámetros disponibles (facturas rectificativas, TicketBAI, webhooks, etc.)
- Revisa las listas de valores (
/api/listas) para conocer todos los códigos válidos deTipoFactura,Impuesto,ClaveRegimenyCalificacionOperacion
¿Listo para probarlo?
Crea tu cuenta gratuita y emite tu primera factura verificada en menos de 10 minutos. El entorno de pruebas está completamente habilitado desde el primer día: sin tarjeta, sin límites artificiales, con la misma API que usarás en producción.