Official, but experimental — AI-generated and AI-maintained. This is an official Apify client, but it is experimental: it is generated and maintained by AI. Review the code before relying on it in production and report issues on the repository.
A resource-oriented Java client for the Apify API, mirroring the
official JavaScript reference client: start from an
ApifyClient, then drill down into resources (Actors, runs, datasets, key-value stores, request
queues, tasks, schedules, webhooks, the store, users and logs).
- Java 17 or newer.
The client is published to Maven Central.
Maven (Maven Central is a default repository, so no extra configuration is needed):
<dependency>
<groupId>com.apify</groupId>
<artifactId>apify-client</artifactId>
<version>0.5.0</version>
</dependency>Gradle — ensure mavenCentral() is in your repositories, then add the dependency:
repositories {
mavenCentral()
}
dependencies {
implementation 'com.apify:apify-client:0.5.0'
}After adding the dependency (above), create an ApifyClient, then drill down into a resource:
import com.apify.client.ApifyClient;
import com.apify.client.run.ActorRun;
import com.apify.client.actor.ActorStartOptions;
class HelloApify {
public static void main(String[] args) {
// Your API token from https://console.apify.com/settings/integrations
ApifyClient client = ApifyClient.create(System.getenv("APIFY_TOKEN"));
ActorRun run = client.actor("apify/hello-world").call(null, new ActorStartOptions(), 120L).join();
System.out.println("Run " + run.getId() + " finished with status " + run.getStatus());
}
}ApifyClient.create takes the token as an explicit argument — it does not read APIFY_TOKEN (or
any other environment variable) automatically. Read it yourself if you want that, e.g.
ApifyClient.create(System.getenv("APIFY_TOKEN")).
All public client types live under com.apify.client, split by resource into sub-packages (e.g.
com.apify.client.run.ActorRun, com.apify.client.dataset.DatasetListItemsOptions) — see
Resources below for the full list; docs/README.md
enumerates the model/option-type packages. The Quick start example above is a complete, runnable
program (imports, class, main); every other snippet in this file, from here on, is a fragment
that assumes a configured client and the correct imports for the types it uses — not, by itself,
a complete program — the resource pages show the same kind of fragment per
method. Reading items from a run's default dataset, chaining the two asynchronous calls:
client.actor("apify/hello-world").call(null, new ActorStartOptions(), 120L)
.thenCompose(run -> client.dataset(run.getDefaultDatasetId())
.listItems(new DatasetListItemsOptions()))
.thenAccept(items -> System.out.println("Items in this page: " + items.getCount()))
.join();The types used above — PaginationList<T> (root package), DatasetListItemsOptions
(com.apify.client.dataset), and the per-resource clients — are documented on the
resource pages; ApifyApiException (com.apify.client.http) is covered under
Error handling below. docs/examples.md has more fragments
in the same style (build-and-run, storages, log redirection, and more); the complete, runnable
programs live under
src/test/java/com/apify/client/examples/.
Every network-calling method returns a
CompletableFuture
(java.util.concurrent.CompletableFuture) instead of blocking the calling thread on the HTTP
exchange, and every paginated collection's iterate(...) returns a
Flow.Publisher
(java.util.concurrent.Flow.Publisher) — the JDK's built-in Reactive Streams type — instead of a
blocking Iterator, so following a large collection never blocks a thread waiting on the next
page's network round-trip.
-
Chain calls with
thenApply/thenCompose/thenAccept(see the dataset example above) to keep a pipeline fully asynchronous end to end. -
Block for a result with
future.join()(unchecked) orfuture.get()(checked,InterruptedException/ExecutionException) when you just want a synchronous-looking call site (e.g. amainmethod, a test, or a simple script) — every snippet in this documentation that needs a value calls.join()for exactly that reason. -
Consume an
iterate(...)publisher either by implementingFlow.Subscriber<T>yourself (onSubscribe→request(n)to pullnitems,onNextper item,onComplete/onErrorto finish) for real backpressure, or with the small convenience bridgecom.apify.client.Publishers.collect(publisher), which subscribes with unbounded demand and returns aCompletableFuture<List<T>>— useful when you just want "give me everything" and don't need backpressure:List<Actor> actors = Publishers.collect(client.actors().iterate(new ActorListOptions())).join();
A future/publisher's failure mode is unchanged from before: any ApifyApiException/
ApifyTransportException this client throws now arrives as the future's exceptional completion
(surfaced by .join()/.get() as a CompletionException/ExecutionException wrapping the
original, still-unchecked exception) instead of a direct throw — see
Error handling below for the full exception hierarchy, which is otherwise
unchanged.
Use ApifyClient.builder() for non-default settings:
ApifyClient configured =
ApifyClient.builder()
.token("my-api-token")
.baseUrl("https://api.apify.com") // /v2 is appended automatically
.maxRetries(8)
.minDelayBetweenRetries(Duration.ofMillis(500))
.timeout(Duration.ofSeconds(360))
.userAgentSuffix("MyTool/1.0")
.build();Duration above is java.time.Duration.
Every setter is optional; leaving one uncalled keeps its default. int/Duration arguments are
validated at call time (see below), not deferred to build().
| Setter | Type | Default | Valid range |
|---|---|---|---|
token(String) |
String |
none (unauthenticated) | any non-null string |
baseUrl(String) |
String |
https://api.apify.com (/v2 appended automatically) |
non-null, non-blank |
publicBaseUrl(String) |
String |
same as baseUrl |
non-null, non-blank |
maxRetries(int) |
int |
8 |
>= 0 |
minDelayBetweenRetries(Duration) |
Duration |
500ms |
non-null, >= 0 |
maxDelayBetweenRetries(Duration) |
Duration |
same as timeout (360s) |
non-null, >= 0 |
timeout(Duration) |
Duration |
360s |
non-null, strictly > 0 |
userAgentSuffix(String) |
String |
none (no suffix appended) | any string |
httpTransport(HttpTransport) |
HttpTransport |
new DefaultHttpTransport() |
non-null |
A violated range throws IllegalArgumentException from the setter itself. timeout is the ceiling
each retry attempt's per-request (socket) timeout grows toward — not a wall-clock bound on the
cumulative time across all retries; the connection-establishment timeout is a separate,
transport-level setting (see DefaultHttpTransport's constructors below).
publicBaseUrl does not affect ordinary API requests (those always use baseUrl); it only changes
the origin embedded in the handful of public, directly-fetchable URLs the client builds without a
request — DatasetClient.createItemsPublicUrl(...) and KeyValueStoreClient.getRecordPublicUrl(...)/
createKeysPublicUrl(...) (see Storages). Set it when the API is reached
through one origin (e.g. an internal proxy) but those public URLs must point at a different,
externally-reachable one.
The transport is a replaceable component. The default is DefaultHttpTransport (backed by the JDK's
java.net.http.HttpClient); provide your own HttpTransport to share a connection pool or customize
proxy/TLS:
HttpTransport transport = new DefaultHttpTransport(java.net.http.HttpClient.newHttpClient());
ApifyClient withTransport = ApifyClient.builder().token("t").httpTransport(transport).build();DefaultHttpTransport also has a DefaultHttpTransport(Duration connectTimeout) constructor that
builds its own JDK HttpClient with a custom connection-establishment timeout (default 10s,
DefaultHttpTransport.DEFAULT_CONNECT_TIMEOUT) without requiring you to construct the HttpClient
yourself — use it when you only want to change the connect timeout and don't otherwise need to
customize the JDK client:
HttpTransport transport = new DefaultHttpTransport(Duration.ofSeconds(5));Cross-cutting behaviour applied to every request lives in the client, not the transport
implementation: bearer-token authentication, the mandated User-Agent header, and retries with
exponential backoff and jitter on 429, 5xx and network errors.
A custom implementation (com.apify.client.http.HttpTransport) must provide both methods, both
non-blocking. HttpRequest and HttpResponse<T> below are the JDK's java.net.http.HttpRequest/
java.net.http.HttpResponse<T> (not custom client types) — same as Duration above:
| Method | Signature | Contract |
|---|---|---|
sendAsync |
CompletableFuture<HttpResponse<byte[]>> sendAsync(HttpRequest request) |
Sends the single, fully-prepared request (auth, User-Agent and any other headers are already set by the client) and completes the future with the response with its body fully buffered as byte[]. Used for every non-streaming call. |
sendStreamingAsync |
CompletableFuture<HttpResponse<InputStream>> sendStreamingAsync(HttpRequest request) |
Sends the request and completes the future with the response body as a live InputStream for incremental consumption, used by log streaming (LogClient.stream(...)). The caller (the client) is responsible for closing the returned stream; an implementation must not close it itself before completing the future. |
Both methods perform exactly one network round-trip each — no retrying, no request mutation
(retries, auth, and the User-Agent header are handled one layer up, by the client itself), and
neither may block the calling thread waiting on the network. A non-2xx HTTP status is not a
transport-level error: complete the future normally with it, as a normal HttpResponse carrying its
actual status code. Only fail the future for a genuine transport failure — connection refused, DNS
resolution failure, or a timeout. Any exception the future is completed exceptionally with is
translated by the client into an ApifyTransportException; complete exceptionally with
com.apify.client.http.HttpTimeoutException specifically for a timeout so
ApifyTransportException.isTimeout() reports it correctly. DefaultHttpTransport implements both
methods directly on top of the JDK HttpClient's own sendAsync.
Methods that fetch a single resource complete with an Optional<T>: a missing resource is reported
by an empty Optional rather than an exception.
client.actor("apify/hello-world").get()
.thenAccept(actor -> actor.ifPresent(a -> System.out.println(a.getTitle())))
.join();Every exception this client throws for a request/transport failure is an unchecked
com.apify.client.http.ApifyClientException. It has two concrete subtypes, both also in
com.apify.client.http. Since every network-calling method is asynchronous (see
Asynchronous API above), these exceptions arrive as the returned
CompletableFuture's exceptional completion rather than a direct throw; .join() re-throws the
original exception wrapped in an unchecked CompletionException (.get() wraps it in a checked
ExecutionException instead) — unwrap getCause() to recover the original
ApifyApiException/ApifyTransportException, or use .handle(...)/.exceptionally(...) to react
to it without unwrapping at all:
ApifyApiException— the request reached the API, which answered with a non-success status.ApifyTransportException— the request never produced an API response at all (connection failure, DNS, timeout, or a local failure preparing the request/response, e.g. compression).isTimeout()reports whether the underlying cause was specifically a timeout (backed bycom.apify.client.http.HttpTimeoutException, part of theHttpTransportcontract, not any specific transport implementation's own exception type). Note the name collision: this is a distinct type from the JDK's ownjava.net.http.HttpTimeoutException(whichDefaultHttpTransportcatches internally and translates into this one) — use an explicit, fully-qualified import or a clear alias if a file needs both.
Catch ApifyClientException to handle both failure modes uniformly, or catch a specific subtype to
handle one of them differently. ApifyApiException is imported from com.apify.client.http:
A few methods validate their own preconditions synchronously, before ever building a request or
future, and throw a plain JDK exception directly from the call site instead, not an
ApifyClientException subtype (so no .join()/.get() unwrapping is involved for these):
ApifyClient.setStatusMessage(message, options) throws IllegalStateException if the
ACTOR_RUN_ID environment variable is not set, ApifyClient.me()'s limits methods (limits(),
updateLimits(...), monthlyUsage(...)) throw the same IllegalStateException if called on a
UserClient that is not me(), and ApifyClientBuilder's setters throw IllegalArgumentException
on an invalid configuration value (e.g. a negative retry count). These are local, no-request-sent
failures, not something the API responded with or the transport failed to deliver, which is why
they stay outside the request/transport exception hierarchy above — catch
IllegalStateException/IllegalArgumentException separately if you call any of them.
try {
client.actor("does/not-exist").update(Map.of("title", "x")).join();
} catch (CompletionException e) {
if (e.getCause() instanceof ApifyApiException apiError) {
System.out.println("status=" + apiError.getStatusCode() + " type=" + apiError.getType());
} else {
throw e;
}
}ApifyApiException exposes the parsed error details:
| Accessor | Type | Meaning |
|---|---|---|
getStatusCode() |
int |
HTTP status code of the error response. |
getType() |
String (nullable) |
Machine-readable error type (e.g. record-not-found). |
getMessage() |
String |
Human-readable description (also Throwable.getMessage()). |
getAttempt() |
int |
The (1-based) attempt number that produced the error. |
getHttpMethod() |
String |
The request's HTTP method. |
getPath() |
String |
The request's URL path. |
getData() |
Map<String, Object> (nullable, unmodifiable) |
Additional structured error data, if any. |
The public com.apify.client.Version class (import com.apify.client.Version;) exposes two
constants:
Version.CLIENT_VERSION— the semantic version of this client (0.5.0).Version.API_SPEC_VERSION— the version of the Apify OpenAPI specification (itsinfo.versionfield) that this client's endpoints, parameters and models were last generated and checked against (v2-2026-07-22T122437Z). It is a snapshot, not a live compatibility guarantee: the client keeps working against newer, backward-compatible spec revisions, but a feature added to the API after this snapshot has no corresponding method here yet.
Changes to the public interface other than additive ones are considered breaking changes and follow Semantic Versioning.
Releases are published to Maven Central through the Sonatype Central Publisher Portal by the
manually-triggered Publish Java client GitHub Actions workflow. The workflow runs in the
protected Publishing GitHub environment and authenticates to the portal with the Maven Central
repository credentials held there, signs the artifacts with GPG, publishes the
com.apify:apify-client artifact, and creates a tagged GitHub release. The release version is taken
from the <version> in pom.xml.
The client covers the documented Apify API endpoints that the JavaScript reference client exposes.
For cross-client parity, the following documented spec endpoints are intentionally not implemented (the JS reference exposes none of them):
- The synchronous run endpoints (
run-sync,run-sync-get-dataset-items). - The cryptographic tools
POST /v2/tools/encode-and-signandPOST /v2/tools/decode-and-verify(this client performs the same HMAC-SHA256 URL signing locally). /v2/browser-info.- The keyed-
POSTcreate variants that duplicate the coveredPUTwrites.
Full documentation is in the docs/ directory, organized by resource:
- Actors, versions & environment variables
- Builds
- Runs
- Storages (datasets, key-value stores, request queues)
- Tasks
- Schedules
- Webhooks & dispatches
- Store, users & logs
- Examples
Every resource client lives in its own sub-package of com.apify.client, named after the resource.
ApifyClient itself, its builder, the exception types, and shared value types (PaginationList,
Version, ...) stay in the root com.apify.client package.
| Accessor | Client | Package | Description |
|---|---|---|---|
actors() / actor(id) |
ActorCollectionClient / ActorClient |
com.apify.client.actor |
Actors |
builds() / build(id) |
BuildCollectionClient / BuildClient |
com.apify.client.build |
Actor builds |
runs() / run(id) |
RunCollectionClient / RunClient |
com.apify.client.run |
Actor runs |
datasets() / dataset(id) |
DatasetCollectionClient / DatasetClient |
com.apify.client.dataset |
Datasets |
keyValueStores() / keyValueStore(id) |
KeyValueStoreCollectionClient / KeyValueStoreClient |
com.apify.client.keyvalue |
Key-value stores |
requestQueues() / requestQueue(id) |
RequestQueueCollectionClient / RequestQueueClient |
com.apify.client.requestqueue |
Request queues |
tasks() / task(id) |
TaskCollectionClient / TaskClient |
com.apify.client.task |
Actor tasks |
schedules() / schedule(id) |
ScheduleCollectionClient / ScheduleClient |
com.apify.client.schedule |
Schedules |
webhooks() / webhook(id) |
WebhookCollectionClient / WebhookClient |
com.apify.client.webhook |
Webhooks |
webhookDispatches() / webhookDispatch(id) |
WebhookDispatchCollectionClient / WebhookDispatchClient |
com.apify.client.webhook |
Webhook dispatches |
store() |
StoreCollectionClient |
com.apify.client.store |
Apify Store |
me() / user(id) |
UserClient |
com.apify.client.user |
Users |
log(id) |
LogClient |
com.apify.client.log |
Build/run logs |
setStatusMessage(message, options) |
— (direct ApifyClient method, no resource client) |
com.apify.client.run (for SetStatusMessageOptions) |
Sets the status message of the current Actor run (see docs/README.md) |
The HTTP transport contract (HttpTransport, DefaultHttpTransport) and the exceptions thrown for
transport-level failures (ApifyTransportException, HttpTimeoutException) live in
com.apify.client.http, alongside ApifyApiException.