diff --git a/src/main/java/com/stripe/model/SearchPagingIterable.java b/src/main/java/com/stripe/model/SearchPagingIterable.java
new file mode 100644
index 00000000000..f99803d35ff
--- /dev/null
+++ b/src/main/java/com/stripe/model/SearchPagingIterable.java
@@ -0,0 +1,22 @@
+package com.stripe.model;
+
+import java.util.Iterator;
+
+/**
+ * Provides an {@code Iterable}
target that automatically iterates across all API
+ * pages and which is suitable for use with a {@code foreach}
loop.
+ *
+ *
Please note SearchPagingIterable is in beta and is subject to change or removal at any time.
+ */
+public class SearchPagingIterable implements Iterable {
+ private StripeSearchResultInterface page;
+
+ SearchPagingIterable(final StripeSearchResultInterface page) {
+ this.page = page;
+ }
+
+ @Override
+ public Iterator iterator() {
+ return new SearchPagingIterator<>(page);
+ }
+}
diff --git a/src/main/java/com/stripe/model/SearchPagingIterator.java b/src/main/java/com/stripe/model/SearchPagingIterator.java
new file mode 100644
index 00000000000..1e45b76ad5d
--- /dev/null
+++ b/src/main/java/com/stripe/model/SearchPagingIterator.java
@@ -0,0 +1,82 @@
+package com.stripe.model;
+
+import com.stripe.Stripe;
+import com.stripe.net.ApiResource;
+import com.stripe.net.RequestOptions;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.NoSuchElementException;
+
+/** Please note SearchPagingIterator is in beta and is subject to change or removal at any time. */
+public class SearchPagingIterator extends ApiResource implements Iterator {
+ private final String url;
+
+ @SuppressWarnings("rawtypes")
+ private final Class extends StripeSearchResultInterface> collectionType;
+
+ private StripeSearchResultInterface currentSearchResult;
+ private Iterator currentDataIterator;
+
+ private String nextPage;
+
+ SearchPagingIterator(final StripeSearchResultInterface stripeSearchResult) {
+ this.url = Stripe.getApiBase() + stripeSearchResult.getUrl();
+ this.nextPage = stripeSearchResult.getNextPage();
+
+ this.collectionType = stripeSearchResult.getClass();
+
+ this.currentSearchResult = stripeSearchResult;
+ this.currentDataIterator = stripeSearchResult.getData().iterator();
+ }
+
+ @Override
+ public boolean hasNext() {
+ return currentDataIterator.hasNext() || currentSearchResult.getHasMore();
+ }
+
+ @Override
+ public T next() {
+ // if we've run out of data on the current page, try to fetch another
+ // one
+ if (!currentDataIterator.hasNext() && currentSearchResult.getHasMore()) {
+ try {
+ Map params = new HashMap<>();
+
+ // copy all the parameters from the initial request
+ Map initialParams = currentSearchResult.getRequestParams();
+ if (initialParams != null) {
+ params.putAll(initialParams);
+ }
+
+ // then put our new page start in
+ params.put("next_page", this.nextPage);
+
+ this.currentSearchResult = search(params, currentSearchResult.getRequestOptions());
+ this.nextPage = this.currentSearchResult.getNextPage();
+
+ this.currentDataIterator = currentSearchResult.getData().iterator();
+ } catch (final Exception e) {
+ throw new RuntimeException("Unable to lazy-load stripe objects", e);
+ }
+ }
+
+ if (currentDataIterator.hasNext()) {
+ final T next = currentDataIterator.next();
+ return next;
+ }
+
+ throw new NoSuchElementException();
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+
+ @SuppressWarnings("unchecked")
+ private StripeSearchResultInterface search(
+ final Map params, final RequestOptions options) throws Exception {
+ return ApiResource.requestSearchResult(url, params, collectionType, options);
+ }
+}
diff --git a/src/main/java/com/stripe/model/StripeSearchResult.java b/src/main/java/com/stripe/model/StripeSearchResult.java
new file mode 100644
index 00000000000..8c0c563f61a
--- /dev/null
+++ b/src/main/java/com/stripe/model/StripeSearchResult.java
@@ -0,0 +1,63 @@
+package com.stripe.model;
+
+import com.stripe.net.RequestOptions;
+import java.util.List;
+import java.util.Map;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * Provides a representation of a single page worth of data from a Stripe API search method. Please
+ * note, StripeSearchResult is beta functionality and is subject to change or removal at any time.
+ */
+@Getter
+@Setter
+@EqualsAndHashCode(callSuper = false)
+public abstract class StripeSearchResult extends StripeObject
+ implements StripeSearchResultInterface {
+ String object;
+
+ @Getter(onMethod_ = {@Override})
+ List data;
+
+ @Getter(onMethod_ = {@Override})
+ Boolean hasMore;
+
+ @Getter(onMethod_ = {@Override})
+ String url;
+
+ @Getter(onMethod_ = {@Override})
+ String nextPage;
+
+ @Getter(onMethod_ = {@Override})
+ @Setter(onMethod = @__({@Override}))
+ private transient RequestOptions requestOptions;
+
+ @Getter(onMethod_ = {@Override})
+ @Setter(onMethod = @__({@Override}))
+ private Map requestParams;
+
+ public Iterable autoPagingIterable() {
+ return new SearchPagingIterable<>(this);
+ }
+
+ public Iterable autoPagingIterable(Map params) {
+ this.setRequestParams(params);
+ return new SearchPagingIterable<>(this);
+ }
+
+ /**
+ * Constructs an iterable that can be used to iterate across all objects across all pages. As page
+ * boundaries are encountered, the next page will be fetched automatically for continued
+ * iteration.
+ *
+ * @param params request parameters (will override the parameters from the initial list request)
+ * @param options request options (will override the options from the initial list request)
+ */
+ public Iterable autoPagingIterable(Map params, RequestOptions options) {
+ this.setRequestOptions(options);
+ this.setRequestParams(params);
+ return new SearchPagingIterable<>(this);
+ }
+}
diff --git a/src/main/java/com/stripe/model/StripeSearchResultInterface.java b/src/main/java/com/stripe/model/StripeSearchResultInterface.java
new file mode 100644
index 00000000000..01caf0915cd
--- /dev/null
+++ b/src/main/java/com/stripe/model/StripeSearchResultInterface.java
@@ -0,0 +1,9 @@
+package com.stripe.model;
+
+/**
+ * Please note, StripeSearchResultInterface is beta functionality and is subject to change or
+ * removal at any time.
+ */
+public interface StripeSearchResultInterface extends StripeCollectionInterface {
+ String getNextPage();
+}
diff --git a/src/main/java/com/stripe/net/ApiResource.java b/src/main/java/com/stripe/net/ApiResource.java
index c23e7a5beed..0b241340f70 100644
--- a/src/main/java/com/stripe/net/ApiResource.java
+++ b/src/main/java/com/stripe/net/ApiResource.java
@@ -21,6 +21,7 @@
import com.stripe.model.StripeObjectInterface;
import com.stripe.model.StripeRawJsonObject;
import com.stripe.model.StripeRawJsonObjectDeserializer;
+import com.stripe.model.StripeSearchResultInterface;
import com.stripe.util.StringUtils;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
@@ -223,6 +224,35 @@ public static > T requestCollection(
return collection;
}
+ public static > T requestSearchResult(
+ String url, ApiRequestParams params, Class clazz, RequestOptions options)
+ throws StripeException {
+ checkNullTypedParams(url, params);
+ return requestSearchResult(url, params.toMap(), clazz, options);
+ }
+
+ /**
+ * Similar to #request, but specific for use with searchResult types that come from the API
+ *
+ * SearchResults, like collections need a little extra work because we need to plumb request
+ * options and params through so that we can iterate to the next page if necessary.
+ *
+ *
Please note, requestSearchResult is beta functionality and is subject to charge or removal
+ * at any time.
+ */
+ public static > T requestSearchResult(
+ String url, Map params, Class clazz, RequestOptions options)
+ throws StripeException {
+ T searchResult = request(RequestMethod.GET, url, params, clazz, options);
+
+ if (searchResult != null) {
+ searchResult.setRequestOptions(options);
+ searchResult.setRequestParams(params);
+ }
+
+ return searchResult;
+ }
+
/**
* Invalidate null typed parameters.
*