diff --git a/CHANGELOG.md b/CHANGELOG.md
index 79a7267..61cd353 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [2.2.0] - 2026-09-09
+
+### Added
+
+- Expose cursor pagination and capped-total metadata on `SearchResult`: `totalsAreCapped`, `nextCursor`, and `previousCursor` so callers can follow cursor pagination and detect capped totals (page totals capped; cursor mode returns totals only on the first page).
+
+### Fixed
+
+- Serialize nested query map values with bracket notation. A date range passed as a nested `Map` (for example `date -> gte/lt`) was stringified with `Map.toString()`; values now expand to bracket keys (`date[gte]=...&date[lt]=...`).
+
## [2.1.0] - 2026-09-04
### Added
diff --git a/pom.xml b/pom.xml
index de17cc0..2a1fa3f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
io.facturapi
facturapi-java
- 2.1.0
+ 2.2.0
facturapi-java
Official Java SDK for Facturapi
https://github.com/facturapi/facturapi-java
diff --git a/src/main/java/io/facturapi/http/FacturapiHttpClient.java b/src/main/java/io/facturapi/http/FacturapiHttpClient.java
index a25c3b4..30b2c48 100644
--- a/src/main/java/io/facturapi/http/FacturapiHttpClient.java
+++ b/src/main/java/io/facturapi/http/FacturapiHttpClient.java
@@ -345,23 +345,7 @@ private static String buildQuery(Map queryParams) {
List parts = new ArrayList<>();
for (Map.Entry entry : queryParams.entrySet()) {
- String key = entry.getKey();
- Object value = entry.getValue();
- if (value == null) {
- continue;
- }
- if (value instanceof Iterable>) {
- for (Object v : (Iterable>) value) {
- appendQueryPart(parts, key, v);
- }
- } else if (value.getClass().isArray()) {
- int length = java.lang.reflect.Array.getLength(value);
- for (int i = 0; i < length; i++) {
- appendQueryPart(parts, key, java.lang.reflect.Array.get(value, i));
- }
- } else {
- appendQueryPart(parts, key, value);
- }
+ appendQueryPart(parts, entry.getKey(), entry.getValue());
}
return parts.isEmpty() ? "" : "?" + String.join("&", parts);
@@ -371,6 +355,25 @@ private static void appendQueryPart(List parts, String key, Object value
if (value == null) {
return;
}
+ if (value instanceof Map, ?>) {
+ for (Map.Entry, ?> entry : ((Map, ?>) value).entrySet()) {
+ appendQueryPart(parts, key + "[" + entry.getKey() + "]", entry.getValue());
+ }
+ return;
+ }
+ if (value instanceof Iterable>) {
+ for (Object item : (Iterable>) value) {
+ appendQueryPart(parts, key, item);
+ }
+ return;
+ }
+ if (value.getClass().isArray()) {
+ int length = java.lang.reflect.Array.getLength(value);
+ for (int i = 0; i < length; i++) {
+ appendQueryPart(parts, key, java.lang.reflect.Array.get(value, i));
+ }
+ return;
+ }
String encodedKey = URLEncoder.encode(Objects.toString(key), StandardCharsets.UTF_8);
String encodedValue = URLEncoder.encode(Objects.toString(value), StandardCharsets.UTF_8);
parts.add(encodedKey + "=" + encodedValue);
diff --git a/src/main/java/io/facturapi/models/SearchResult.java b/src/main/java/io/facturapi/models/SearchResult.java
index 2701e72..b36b57b 100644
--- a/src/main/java/io/facturapi/models/SearchResult.java
+++ b/src/main/java/io/facturapi/models/SearchResult.java
@@ -2,7 +2,6 @@
import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.ArrayList;
import java.util.List;
@@ -12,7 +11,10 @@ public class SearchResult {
private Integer totalPages;
@JsonAlias({"total"})
private Integer totalResults;
+ private Boolean totalsAreCapped;
private Boolean hasMore;
+ private String nextCursor;
+ private String previousCursor;
private List data = new ArrayList<>();
public Integer getPage() {
@@ -39,6 +41,14 @@ public void setTotalResults(Integer totalResults) {
this.totalResults = totalResults;
}
+ public Boolean getTotalsAreCapped() {
+ return totalsAreCapped;
+ }
+
+ public void setTotalsAreCapped(Boolean totalsAreCapped) {
+ this.totalsAreCapped = totalsAreCapped;
+ }
+
public Boolean getHasMore() {
return hasMore;
}
@@ -47,6 +57,22 @@ public void setHasMore(Boolean hasMore) {
this.hasMore = hasMore;
}
+ public String getNextCursor() {
+ return nextCursor;
+ }
+
+ public void setNextCursor(String nextCursor) {
+ this.nextCursor = nextCursor;
+ }
+
+ public String getPreviousCursor() {
+ return previousCursor;
+ }
+
+ public void setPreviousCursor(String previousCursor) {
+ this.previousCursor = previousCursor;
+ }
+
public List getData() {
return data;
}
diff --git a/src/test/java/io/facturapi/FacturapiHttpClientTest.java b/src/test/java/io/facturapi/FacturapiHttpClientTest.java
index e8d6eac..fb45de9 100644
--- a/src/test/java/io/facturapi/FacturapiHttpClientTest.java
+++ b/src/test/java/io/facturapi/FacturapiHttpClientTest.java
@@ -2,6 +2,7 @@
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -78,6 +79,31 @@ void throwsFacturapiExceptionWithApiMessage() {
assertEquals("log_123", ex.getHeaders().get("x-facturapi-log-id").get(0));
}
+ @Test
+ void serializesNestedDateRangeQueryWithBracketNotation() throws Exception {
+ StubHttpClient httpClient = new StubHttpClient();
+ httpClient.enqueueJson(200, "{\"data\":[]}");
+
+ FacturapiHttpClient client = new FacturapiHttpClient(
+ FacturapiConfig.builder("sk_test_123")
+ .httpClient(httpClient.client())
+ .build()
+ );
+
+ client.get(
+ "/invoices",
+ Map.of("limit", 100, "date", Map.of("gte", "2026-01-01", "lt", "2026-02-01")),
+ GenericResponse.class
+ );
+
+ var request = httpClient.requests().get(0);
+ String query = request.uri().getQuery();
+ assertTrue(query.contains("limit=100"));
+ assertTrue(query.contains("date[gte]=2026-01-01"));
+ assertTrue(query.contains("date[lt]=2026-02-01"));
+ assertFalse(query.contains("{gte"));
+ }
+
@Test
void convertsNumericApiErrorCodesToStrings() {
StubHttpClient httpClient = new StubHttpClient();
diff --git a/src/test/java/io/facturapi/FacturapiResourcesTest.java b/src/test/java/io/facturapi/FacturapiResourcesTest.java
index e425076..7656160 100644
--- a/src/test/java/io/facturapi/FacturapiResourcesTest.java
+++ b/src/test/java/io/facturapi/FacturapiResourcesTest.java
@@ -3,6 +3,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import io.facturapi.enums.CancellationStatus;
@@ -539,6 +540,29 @@ void objectMapperDeserializesCodeEnums() throws Exception {
assertEquals(TaxFactor.EXENTO, tax.getFactor());
}
+ @Test
+ void invoiceListDeserializesSearchResultPaginationMetadata() throws Exception {
+ StubHttpClient httpClient = new StubHttpClient();
+ httpClient.enqueueJson(
+ 200,
+ "{\"data\":[],\"total_results\":3000,\"totals_are_capped\":true,\"next_cursor\":\"next-1\",\"previous_cursor\":null}"
+ );
+
+ Facturapi sdk = new Facturapi(
+ FacturapiConfig.builder("sk_test")
+ .httpClient(httpClient.client())
+ .build()
+ );
+
+ var result = sdk.invoices().list(Map.of());
+
+ assertTrue(result.getTotalsAreCapped());
+ assertEquals(3000, result.getTotalResults());
+ assertEquals("next-1", result.getNextCursor());
+ assertNull(result.getPreviousCursor());
+ assertTrue(result.getData().isEmpty());
+ }
+
@Test
void objectMapperDeserializesPropertyTaxAccountsAsArrays() throws Exception {
var mapper = FacturapiConfig.builder("sk_test").build().getObjectMapper();