Skip to Content
v1.5Official documentation for the LX CloudPos API · stable in production

Java

Complete client on top of the JDK’s HttpClient (Java 17 or higher), with no external networking dependencies: only Jackson to serialize JSON. Includes issuing, querying, downloading PDF and XML, resending email, voiding, next consecutive number, and a webhook receiver with HMAC verification.

Setup

<!-- pom.xml --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.17.1</version> </dependency>

Complete client

LxsClient maintains a single HttpClient with a 30 s timeout and adds the authentication headers on each call. Each issuance generates its own Idempotency-Key, unless one is provided to retry the same operation.

// LxsClient.java package cr.lxsyscr.integracion; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; import java.util.List; import java.util.UUID; public class LxsClient { private static final String BASE_URL = "https://api.pos.lxsyscr.com"; private final HttpClient http; private final ObjectMapper mapper; private final String apiKey; private final String baseUrl; public LxsClient(String apiKey) { this(apiKey, BASE_URL); } public LxsClient(String apiKey, String baseUrl) { this.apiKey = apiKey; this.baseUrl = baseUrl; this.http = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(10)) .build(); this.mapper = new ObjectMapper() .setSerializationInclusion(JsonInclude.Include.NON_NULL); } // ── Issuance ────────────────────────────────────────────────────────── public FacturaResponse emitir(FacturaRequest data) throws Exception { return emitir(data, UUID.randomUUID().toString()); } /** The idempotencyKey must be generated ONCE and reused on every retry. */ public FacturaResponse emitir(FacturaRequest data, String idempotencyKey) throws Exception { HttpRequest req = base("/api/v1/facturas") .header("Content-Type", "application/json") .header("Idempotency-Key", idempotencyKey) .POST(HttpRequest.BodyPublishers.ofString(mapper.writeValueAsString(data))) .build(); HttpResponse<String> res = http.send(req, HttpResponse.BodyHandlers.ofString()); exigirExito(res); return mapper.readValue(res.body(), FacturaResponse.class); } // ── Query and downloads ───────────────────────────────────────────── public FacturaResponse consultar(String clave) throws Exception { HttpResponse<String> res = http.send( base("/api/v1/facturas/" + clave).GET().build(), HttpResponse.BodyHandlers.ofString()); exigirExito(res); return mapper.readValue(res.body(), FacturaResponse.class); } public byte[] descargarPdf(String clave) throws Exception { HttpResponse<byte[]> res = http.send( base("/api/v1/facturas/" + clave + "/pdf").GET().build(), HttpResponse.BodyHandlers.ofByteArray()); if (res.statusCode() >= 400) { throw new LxsException(res.statusCode(), new String(res.body())); } return res.body(); } public String descargarXml(String clave) throws Exception { HttpResponse<String> res = http.send( base("/api/v1/facturas/" + clave + "/xml").GET().build(), HttpResponse.BodyHandlers.ofString()); exigirExito(res); return res.body(); } /** Receipt signed by Hacienda. Returns null while it responds with 202. */ public String descargarRespuestaHacienda(String clave) throws Exception { HttpResponse<String> res = http.send( base("/api/v1/facturas/" + clave + "/respuesta").GET().build(), HttpResponse.BodyHandlers.ofString()); if (res.statusCode() == 202) { return null; } exigirExito(res); return res.body(); } // ── Other operations ──────────────────────────────────────────────── public void reenviarEmail(String clave) throws Exception { HttpResponse<String> res = http.send( base("/api/v1/facturas/" + clave + "/email") .POST(HttpRequest.BodyPublishers.noBody()).build(), HttpResponse.BodyHandlers.ofString()); exigirExito(res); } public void anular(String clave) throws Exception { HttpResponse<String> res = http.send( base("/api/v1/facturas/" + clave).DELETE().build(), HttpResponse.BodyHandlers.ofString()); exigirExito(res); } public long proximoConsecutivo(String tipo) throws Exception { HttpResponse<String> res = http.send( base("/api/v1/consecutivos/proximo?tipo=" + tipo).GET().build(), HttpResponse.BodyHandlers.ofString()); exigirExito(res); return mapper.readTree(res.body()).get("proximo").asLong(); } // ── Internal ───────────────────────────────────────────────────────── private HttpRequest.Builder base(String path) { return HttpRequest.newBuilder() .uri(URI.create(baseUrl + path)) .header("X-Api-Key", apiKey) .header("Accept", "application/json") .timeout(Duration.ofSeconds(30)); } private void exigirExito(HttpResponse<String> res) { if (res.statusCode() >= 400) { throw new LxsException(res.statusCode(), res.body()); } } // ── Types ──────────────────────────────────────────────────────────── public record FacturaRequest( String tipo, int idUsuario, Cliente cliente, String condicionVenta, Integer plazoCredito, String medioPago, String moneda, Double tipoCambio, String codigoActividad, String comentario, List<Linea> lineas, List<Referencia> referencias) {} public record Cliente(Integer idCliente, String identificacion, String tipoIdentificacion, String nombre, String correo, String telefono) { public static Cliente porId(int idCliente) { return new Cliente(idCliente, null, null, null, null, null); } } public record Linea(Integer idProducto, String codigoProducto, double cantidad, Double precioUnitario, Double montoDescuento, String naturalezaDescuento) { public static Linea de(int idProducto, double cantidad) { return new Linea(idProducto, null, cantidad, null, null, null); } } public record Referencia(String tipoDocumento, String numero, String fechaEmision, String codigo, String razon) {} public record FacturaResponse( String clave, String numeroConsecutivo, String tipoDocumento, String estadoHacienda, double totalComprobante, String moneda, String fechaEmision) {} public static class LxsException extends RuntimeException { public final int status; public LxsException(int status, String body) { super("HTTP " + status + ": " + body); this.status = status; } } }

Usage

LxsClient lxs = new LxsClient(System.getenv("LXS_API_KEY")); var factura = lxs.emitir(new LxsClient.FacturaRequest( "FacturaElectronica", 7, // idUsuario: required LxsClient.Cliente.porId(42), "01", null, "01", "CRC", null, null, null, List.of(LxsClient.Linea.de(100, 2)), List.of())); System.out.println("Key: " + factura.clave());

Retries with idempotency

The key is generated outside the loop: this way retries reuse the same key and a document is not issued per attempt.

public FacturaResponse emitirConReintentos(LxsClient lxs, FacturaRequest data) throws Exception { String idempotencyKey = UUID.randomUUID().toString(); // just once for (int intento = 0; intento < 3; intento++) { try { return lxs.emitir(data, idempotencyKey); } catch (LxsClient.LxsException e) { boolean recuperable = e.status >= 500 || e.status == 429; if (!recuperable || intento == 2) { throw e; } Thread.sleep(2000L * (intento + 1)); } } throw new IllegalStateException("Reintentos agotados"); }

Webhook receiver (Spring Boot)

The controller receives the raw body as byte[]: if Spring is allowed to deserialize it into an object and then serialize it again, the spacing and key order change, and the HMAC no longer matches. The comparison uses MessageDigest.isEqual, which runs in constant time.

// LxsWebhookController.java package cr.lxsyscr.integracion; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.util.HexFormat; @RestController @RequestMapping("/webhooks/lxs") public class LxsWebhookController { private static final String SECRET = System.getenv("LXS_WEBHOOK_SECRET"); private final ObjectMapper mapper = new ObjectMapper(); @PostMapping public ResponseEntity<Void> handle( @RequestBody byte[] body, @RequestHeader(value = "X-Lxs-Signature", required = false) String signature) throws Exception { if (signature == null || !firmaValida(body, signature)) { return ResponseEntity.status(401).build(); } JsonNode evento = mapper.readTree(body); String nonce = evento.get("nonce").asText(); String clave = evento.get("clave").asText(); String estado = evento.get("ind_estado").asText(); // Respond quickly: Hacienda does not wait, and the issuer retries after 10 s. // Heavy processing goes to a queue, not inside the request. procesarAsync(nonce, clave, estado); return ResponseEntity.ok().build(); } private boolean firmaValida(byte[] body, String header) throws Exception { String recibida = header.startsWith("sha256=") ? header.substring(7) : header; Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(SECRET.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); String esperada = HexFormat.of().formatHex(mac.doFinal(body)); return MessageDigest.isEqual( recibida.getBytes(StandardCharsets.UTF_8), esperada.getBytes(StandardCharsets.UTF_8)); } private void procesarAsync(String nonce, String clave, String estado) { // Deduplicate by nonce before acting: the same event can arrive // more than once if the retry crosses with the response. } }