diff --git a/CHANGELOG.md b/CHANGELOG.md
index c642d4c..dfb9f00 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,21 @@ All notable changes to the Apify Java client are 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).
+## [0.10.0] - 2026-10-21
+
+### Added
+
+- `RunChargeOptions.requestId(String)`, sent as `requestId` in the charge request body: the ID of the
+ Server Actor request being charged, required for multi-tenant Server Actor runs.
+- `Build.getActVersion()` and the `BuildActVersion` model (with the `SourceCodeFile` model for its
+ inline `sourceFiles`); `buildTag` is `null` for a version without a build tag.
+
+### Changed
+
+- Updated to Apify OpenAPI specification `v2-2026-10-20T121847Z`.
+- Documented that the `FLAT_PRICE_PER_MONTH` (rental) pricing model is deprecated and no longer
+ listed as a Store `pricingModel` filter value.
+
## [0.9.0] - 2026-10-19
### Added
diff --git a/README.md b/README.md
index 980125c..c272636 100644
--- a/README.md
+++ b/README.md
@@ -23,7 +23,7 @@ Maven (Maven Central is a default repository, so no extra configuration is neede
com.apify
apify-client
- 0.9.0
+ 0.10.0
```
@@ -35,7 +35,7 @@ repositories {
}
dependencies {
- implementation 'com.apify:apify-client:0.9.0'
+ implementation 'com.apify:apify-client:0.10.0'
}
```
@@ -317,10 +317,10 @@ try {
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.9.0`).
+- `Version.CLIENT_VERSION` — the semantic version of this client (`0.10.0`).
- `Version.API_SPEC_VERSION` — the version of the [Apify OpenAPI specification](https://docs.apify.com/api/openapi.json)
(its `info.version` field) that this client's endpoints, parameters and models were last generated
- and checked against (`v2-2026-10-08T073338Z`). It is a snapshot, not a live compatibility
+ and checked against (`v2-2026-10-20T121847Z`). 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.
diff --git a/docs/README.md b/docs/README.md
index e21c877..04ad78f 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -40,8 +40,8 @@ option types, so import each resource you use from its own package:
|---|---|
| `com.apify.client` (root) | `ApifyClient`, `ApifyClientBuilder`, `Version`, `PaginationList`, `ListOptions`, `StorageListOptions`, `ApifyResource` |
| `com.apify.client.http` | `ApifyClientException`, `ApifyApiException`, `ApifyTransportException`, `HttpTransport`, `DefaultHttpTransport`, `HttpTimeoutException` |
-| `com.apify.client.actor` | `Actor`, `ActorClient`, `ActorCollectionClient`, `ActorListOptions`, `ActorStartOptions`, `ActorCallOptions`, `ActorBuildOptions`, `ActorStandby`, `ActorVersion`, `ActorVersionClient`, `ActorVersionCollectionClient`, `ActorEnvVar`, `ActorEnvVarClient`, `ActorEnvVarCollectionClient`, `ValidateInputOptions`, `ActorStats`, `ActorDefaultRunOptions` |
-| `com.apify.client.build` | `Build`, `BuildClient`, `BuildCollectionClient`, `BuildMeta`, `BuildStats`, `BuildOptions`, `BuildUsage` |
+| `com.apify.client.actor` | `Actor`, `ActorClient`, `ActorCollectionClient`, `ActorListOptions`, `ActorStartOptions`, `ActorCallOptions`, `ActorBuildOptions`, `ActorStandby`, `ActorVersion`, `SourceCodeFile`, `ActorVersionClient`, `ActorVersionCollectionClient`, `ActorEnvVar`, `ActorEnvVarClient`, `ActorEnvVarCollectionClient`, `ValidateInputOptions`, `ActorStats`, `ActorDefaultRunOptions` |
+| `com.apify.client.build` | `Build`, `BuildClient`, `BuildCollectionClient`, `BuildActVersion`, `BuildMeta`, `BuildStats`, `BuildOptions`, `BuildUsage` |
| `com.apify.client.run` | `ActorRun`, `ActorRunStats`, `ActorRunOptions`, `ActorRunMeta`, `ActorRunUsage`, `RunClient`, `RunCollectionClient`, `RunListOptions`, `LastRunOptions`, `MetamorphOptions`, `RunChargeOptions`, `RunResurrectOptions`, `SetStatusMessageOptions` |
| `com.apify.client.dataset` | `Dataset`, `DatasetClient`, `DatasetCollectionClient`, `DatasetListItemsOptions`, `DatasetDownloadOptions`, `DownloadItemsFormat` |
| `com.apify.client.keyvalue` | `KeyValueStore`, `KeyValueStoreClient`, `KeyValueStoreCollectionClient`, `KeyValueStoreRecord`, `KeyValueStoreKeysPage`, `KeyValueStoreKey`, `GetRecordOptions`, `SetRecordOptions`, `ListKeysOptions` |
diff --git a/docs/builds.md b/docs/builds.md
index b5704a4..b1f5d8c 100644
--- a/docs/builds.md
+++ b/docs/builds.md
@@ -23,7 +23,9 @@ builds) and a single build with `client.build(id)`.
| `log()` | A `LogClient` for the build's log. |
`Build` fields: `getId()`, `getActId()`, `getUserId()`, `getStatus()`, `getStartedAt()`,
-`getFinishedAt()`, `getBuildNumber()`, `getImageDigest()` (`String`, nullable — the built Docker
+`getFinishedAt()`, `getBuildNumber()`, `getActVersion()` (`BuildActVersion` — `getSourceType()`, `getBuildTag()`
+(nullable), `getVersionNumber()`, `getGitRepoUrl()`, `getSourceFiles()` (`List` — `getName()`,
+`getFormat()`, `getContent()`)), `getImageDigest()` (`String`, nullable — the built Docker
image manifest's digest, without the `sha256:` prefix; compare two builds' digests to tell whether
their image contents differ), `getMeta()` (`BuildMeta` — `getOrigin()`, `getClientIp()`,
`getUserAgent()`), `getStats()` (`BuildStats` — `getDurationMillis()`/`getRunTimeSecs()` as `Long`,
@@ -32,7 +34,7 @@ their image contents differ), `getMeta()` (`BuildMeta` — `getOrigin()`, `getCl
`getUsage()` / `getUsageUsd()` (`BuildUsage` — `getActorComputeUnits()` as `Double`, the latter as
its USD cost), `getUsageTotalUsd()` (`Double`), plus `isTerminal()` and `getExtra()`. The status is
one of `READY`, `RUNNING`, `SUCCEEDED`, `FAILED`, `TIMING-OUT`, `TIMED-OUT`, `ABORTING`, `ABORTED`.
-Every nested model above (`BuildMeta`, `BuildStats`, `BuildOptions`, `BuildUsage`) also has its own
+Every nested model above (`BuildActVersion`, `BuildMeta`, `BuildStats`, `BuildOptions`, `BuildUsage`) also has its own
`getExtra()`, so a field not yet listed here is still reachable rather than dropped.
```java
diff --git a/docs/misc.md b/docs/misc.md
index 2d86256..63b8773 100644
--- a/docs/misc.md
+++ b/docs/misc.md
@@ -12,7 +12,8 @@ Browse public Actors in the Apify Store. `client.store()` returns a `StoreCollec
`StoreListOptions` fields: `offset` (number of Actors to skip), `limit` (maximum number of Actors to
return), `search` (full-text search query), `sortBy` (the sort field, e.g. `popularity`, `newest`),
`category` (filter Actors by category), `username` (filter Actors by owner username), `pricingModel`
-(filter by pricing model: `FREE`, `FLAT_PRICE_PER_MONTH`, `PRICE_PER_DATASET_ITEM`, `PAY_PER_EVENT`),
+(filter by pricing model: `FREE`, `PRICE_PER_DATASET_ITEM`, `PAY_PER_EVENT`; the deprecated rental model
+`FLAT_PRICE_PER_MONTH` is no longer listed),
`includeUnrunnableActors` (include Actors the current user cannot run), `allowsAgenticUsers` (only
Actors that allow agentic users), `responseFormat` (the response shape: `full` or `agent`).
diff --git a/docs/runs.md b/docs/runs.md
index f406ddf..2668b71 100644
--- a/docs/runs.md
+++ b/docs/runs.md
@@ -151,9 +151,10 @@ nullable), `getMemoryMbytes()` (`Long`, nullable), `getDiskMbytes()` (`Long`, nu
`getMaxItems()` (`Long`, nullable), `getMaxTotalChargeUsd()` (`Double`, nullable),
`getRestartOnError()` (`Boolean`, nullable).
-`RunChargeOptions` (constructed with the required event name) uses plain values: `count(Long)` and
-`idempotencyKey(String)` — the latter is auto-generated when unset so a transport-retried charge is
-applied at most once.
+`RunChargeOptions` (constructed with the required event name) uses plain values: `count(Long)`,
+`idempotencyKey(String)` — auto-generated when unset so a transport-retried charge is applied at
+most once — and `requestId(String)`, the `X-Actor-Request-ID` of the Server Actor request being
+charged (required for multi-tenant Server Actor runs, omitted otherwise).
```java
client.run("RUN_ID").charge(new RunChargeOptions("my-event").count(3L)).join();
diff --git a/pom.xml b/pom.xml
index d6753b4..e189322 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
com.apify
apify-client
- 0.9.0
+ 0.10.0
jar
Apify Java Client
diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml
index 4111b75..cfafadb 100644
--- a/spotbugs-exclude.xml
+++ b/spotbugs-exclude.xml
@@ -18,8 +18,8 @@
2. UWF_UNWRITTEN_FIELD / UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD on Jackson-populated DTOs/response
shapes ({@code KeyValueStoreKey}, {@code DeletedRequestInfo}, {@code ActorRunMeta}, {@code
- TaskOptions}, {@code WebhookLastDispatch}, {@code ActorDefaultRunOptions}, {@code BuildMeta},
- {@code BuildOptions}, {@code BuildStats}, {@code PricingInfo}, {@code ProxyGroup}, {@code
+ TaskOptions}, {@code WebhookLastDispatch}, {@code ActorDefaultRunOptions}, {@code BuildActVersion},
+ {@code BuildMeta}, {@code BuildOptions}, {@code BuildStats}, {@code PricingInfo}, {@code ProxyGroup}, {@code
UserProxy}, {@code WebhookDispatchEventData}, {@code WebhookDispatchWebhookInfo}, the internal
error-response mapping in {@code HttpClientCore}): their fields are written by the
deserializer via reflection, which SpotBugs cannot see - a known false positive for
@@ -74,6 +74,8 @@
+
+
@@ -111,6 +113,7 @@
+
diff --git a/src/main/java/com/apify/client/Version.java b/src/main/java/com/apify/client/Version.java
index fe488ad..95b44ba 100644
--- a/src/main/java/com/apify/client/Version.java
+++ b/src/main/java/com/apify/client/Version.java
@@ -13,13 +13,13 @@ public final class Version {
* The semantic version of this client library (see SemVer).
* Changes to the public interface other than additive ones are considered breaking changes.
*/
- public static final String CLIENT_VERSION = "0.9.0";
+ public static final String CLIENT_VERSION = "0.10.0";
/**
* The version of the Apify OpenAPI specification this client was generated and verified against.
* Corresponds to the {@code info.version} field of the Apify OpenAPI document.
*/
- public static final String API_SPEC_VERSION = "v2-2026-10-08T073338Z";
+ public static final String API_SPEC_VERSION = "v2-2026-10-20T121847Z";
private Version() {}
}
diff --git a/src/main/java/com/apify/client/actor/SourceCodeFile.java b/src/main/java/com/apify/client/actor/SourceCodeFile.java
new file mode 100644
index 0000000..8a1c98f
--- /dev/null
+++ b/src/main/java/com/apify/client/actor/SourceCodeFile.java
@@ -0,0 +1,28 @@
+package com.apify.client.actor;
+
+import com.apify.client.ApifyResource;
+
+/** A single file in an Actor's source code. */
+public final class SourceCodeFile extends ApifyResource {
+ private String name;
+ private String format;
+ private String content;
+
+ /** The path of the file relative to the Actor's root directory. */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * The format of {@link #getContent()}: {@code "TEXT"} for plain text, {@code "BASE64"} for
+ * encoded content.
+ */
+ public String getFormat() {
+ return format;
+ }
+
+ /** The contents of the file, interpreted according to {@link #getFormat()}. */
+ public String getContent() {
+ return content;
+ }
+}
diff --git a/src/main/java/com/apify/client/build/Build.java b/src/main/java/com/apify/client/build/Build.java
index cd12040..7a01932 100644
--- a/src/main/java/com/apify/client/build/Build.java
+++ b/src/main/java/com/apify/client/build/Build.java
@@ -14,6 +14,7 @@ public final class Build extends ApifyResource {
private Instant finishedAt;
private String buildNumber;
private String imageDigest;
+ private BuildActVersion actVersion;
private BuildMeta meta;
private BuildStats stats;
private BuildOptions options;
@@ -69,6 +70,14 @@ public String getImageDigest() {
return imageDigest;
}
+ /**
+ * A snapshot of the Actor version this build was created from. Its {@link
+ * BuildActVersion#getBuildTag()} is {@code null} for a version without a build tag.
+ */
+ public BuildActVersion getActVersion() {
+ return actVersion;
+ }
+
/** Metadata about how the build was initiated. */
public BuildMeta getMeta() {
return meta;
diff --git a/src/main/java/com/apify/client/build/BuildActVersion.java b/src/main/java/com/apify/client/build/BuildActVersion.java
new file mode 100644
index 0000000..2e66599
--- /dev/null
+++ b/src/main/java/com/apify/client/build/BuildActVersion.java
@@ -0,0 +1,50 @@
+package com.apify.client.build;
+
+import com.apify.client.ApifyResource;
+import com.apify.client.actor.SourceCodeFile;
+import java.util.Collections;
+import java.util.List;
+
+/** A snapshot of the Actor version a {@link Build} was created from. */
+public final class BuildActVersion extends ApifyResource {
+ private String sourceType;
+ private String buildTag;
+ private String versionNumber;
+ private String gitRepoUrl;
+ private List sourceFiles;
+
+ /**
+ * How the version's source was provided (e.g. {@code "SOURCE_FILES"}, {@code "GIT_REPO"}, {@code
+ * "TARBALL"}, {@code "GITHUB_GIST"}, {@code "SOURCE_CODE"}).
+ */
+ public String getSourceType() {
+ return sourceType;
+ }
+
+ /**
+ * The build tag of the Actor version the build was created from. {@code null} when the version
+ * has no build tag, in which case the build is tagged on success only if a tag was passed when
+ * starting it.
+ */
+ public String getBuildTag() {
+ return buildTag;
+ }
+
+ /** The version number (e.g. {@code "0.1"}). */
+ public String getVersionNumber() {
+ return versionNumber;
+ }
+
+ /** URL of the Git repository; present when the source type is {@code GIT_REPO}. */
+ public String getGitRepoUrl() {
+ return gitRepoUrl;
+ }
+
+ /**
+ * The inline source files; non-empty only when the source type is {@code SOURCE_FILES}, an empty
+ * list otherwise.
+ */
+ public List getSourceFiles() {
+ return sourceFiles == null ? List.of() : Collections.unmodifiableList(sourceFiles);
+ }
+}
diff --git a/src/main/java/com/apify/client/run/RunChargeOptions.java b/src/main/java/com/apify/client/run/RunChargeOptions.java
index 7a3f64a..6c19fc9 100644
--- a/src/main/java/com/apify/client/run/RunChargeOptions.java
+++ b/src/main/java/com/apify/client/run/RunChargeOptions.java
@@ -5,6 +5,7 @@ public final class RunChargeOptions {
private String eventName;
private Long count;
private String idempotencyKey;
+ private String requestId;
/** Creates options for the given (required) event name. */
public RunChargeOptions(String eventName) {
@@ -32,6 +33,16 @@ public RunChargeOptions idempotencyKey(String idempotencyKey) {
return this;
}
+ /**
+ * The ID of the Server Actor request being charged, taken from the {@code X-Actor-Request-ID}
+ * header of the incoming request. Required for multi-tenant Server Actor runs, where it
+ * identifies the request the charge belongs to; omit it otherwise.
+ */
+ public RunChargeOptions requestId(String requestId) {
+ this.requestId = requestId;
+ return this;
+ }
+
String eventNameValue() {
return eventName;
}
@@ -43,4 +54,8 @@ long countValue() {
String idempotencyKeyValue() {
return idempotencyKey;
}
+
+ String requestIdValue() {
+ return requestId;
+ }
}
diff --git a/src/main/java/com/apify/client/run/RunClient.java b/src/main/java/com/apify/client/run/RunClient.java
index a8dd46b..3bf64a5 100644
--- a/src/main/java/com/apify/client/run/RunClient.java
+++ b/src/main/java/com/apify/client/run/RunClient.java
@@ -172,6 +172,10 @@ public CompletableFuture charge(RunChargeOptions options) {
Map body = new LinkedHashMap<>();
body.put("eventName", eventName);
body.put("count", options.countValue());
+ String requestId = options.requestIdValue();
+ if (requestId != null && !requestId.isEmpty()) {
+ body.put("requestId", requestId);
+ }
Map headers = Map.of(CHARGE_IDEMPOTENCY_HEADER, idempotencyKey);
// Route through mergedParams like the run's other actions, so a last-run-seeded context charges
// the same run its read methods resolve to (its pinned status/origin filters are preserved).
diff --git a/src/main/java/com/apify/client/store/PricingInfo.java b/src/main/java/com/apify/client/store/PricingInfo.java
index 6710933..3d7c728 100644
--- a/src/main/java/com/apify/client/store/PricingInfo.java
+++ b/src/main/java/com/apify/client/store/PricingInfo.java
@@ -7,7 +7,7 @@ public final class PricingInfo extends ApifyResource {
private String pricingModel;
/**
- * The pricing model, e.g. {@code "FREE"}, {@code "FLAT_PRICE_PER_MONTH"}, {@code
+ * The pricing model, e.g. {@code "FREE"}, {@code "PRICE_PER_DATASET_ITEM"}, {@code
* "PAY_PER_EVENT"}.
*/
public String getPricingModel() {
diff --git a/src/main/java/com/apify/client/store/StoreListOptions.java b/src/main/java/com/apify/client/store/StoreListOptions.java
index 785415a..df086b3 100644
--- a/src/main/java/com/apify/client/store/StoreListOptions.java
+++ b/src/main/java/com/apify/client/store/StoreListOptions.java
@@ -56,8 +56,9 @@ public StoreListOptions username(String username) {
}
/**
- * Filter Actors by pricing model ({@code FREE}, {@code FLAT_PRICE_PER_MONTH}, {@code
- * PRICE_PER_DATASET_ITEM}, {@code PAY_PER_EVENT}).
+ * Filter Actors by pricing model ({@code FREE}, {@code PRICE_PER_DATASET_ITEM}, {@code
+ * PAY_PER_EVENT}). The rental model ({@code FLAT_PRICE_PER_MONTH}) is deprecated and no longer
+ * supported, so it is no longer documented as a filter value and matches no Actors.
*/
public StoreListOptions pricingModel(String pricingModel) {
this.pricingModel = pricingModel;
diff --git a/src/test/java/com/apify/client/ClientBehaviourRegressionTest.java b/src/test/java/com/apify/client/ClientBehaviourRegressionTest.java
index 7d19607..62b5771 100644
--- a/src/test/java/com/apify/client/ClientBehaviourRegressionTest.java
+++ b/src/test/java/com/apify/client/ClientBehaviourRegressionTest.java
@@ -119,6 +119,20 @@ void chargeHonorsExplicitIdempotencyKey() {
assertEquals("fixed-key", backend.lastHeaders.firstValue("idempotency-key").orElse(""));
}
+ @Test
+ void chargeSendsRequestIdOnlyWhenSet() {
+ MockTransport withId = MockTransport.ofConstant(200, "{\"data\":{}}");
+ client(withId)
+ .run("run123")
+ .charge(new RunChargeOptions("e").requestId("7Hs2kQw9LxP4mN1vB"))
+ .join();
+ assertTrue(withId.lastBody.contains("\"requestId\":\"7Hs2kQw9LxP4mN1vB\""), withId.lastBody);
+
+ MockTransport without = MockTransport.ofConstant(200, "{\"data\":{}}");
+ client(without).run("run123").charge(new RunChargeOptions("e")).join();
+ assertFalse(without.lastBody.contains("requestId"), without.lastBody);
+ }
+
@Test
void chargeRejectsMissingEventName() {
MockTransport backend = MockTransport.ofConstant(200, "{\"data\":{}}");
diff --git a/src/test/java/com/apify/client/NestedDtoForwardCompatTest.java b/src/test/java/com/apify/client/NestedDtoForwardCompatTest.java
index 4e5167a..64278e2 100644
--- a/src/test/java/com/apify/client/NestedDtoForwardCompatTest.java
+++ b/src/test/java/com/apify/client/NestedDtoForwardCompatTest.java
@@ -1,9 +1,11 @@
package com.apify.client;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import com.apify.client.actor.ActorStandby;
+import com.apify.client.build.Build;
import com.apify.client.build.BuildStats;
import com.apify.client.internal.Json;
import com.apify.client.keyvalue.KeyValueStoreKeysPage;
@@ -14,6 +16,7 @@
import com.apify.client.store.PricingInfo;
import com.apify.client.task.TaskStats;
import com.apify.client.user.UserProfile;
+import com.apify.client.webhook.Webhook;
import com.apify.client.webhook.WebhookStats;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
@@ -73,6 +76,50 @@ void buildStatsModelsImageSizeBytesAndKeepsUnknownFields() {
assertEquals(true, stats.getExtra().get("someFutureStatField"));
}
+ @Test
+ void buildActVersionAcceptsNullBuildTag() {
+ Build tagged =
+ parse(
+ "{\"id\":\"b1\",\"actVersion\":{\"sourceType\":\"GIT_REPO\","
+ + "\"buildTag\":\"latest\",\"versionNumber\":\"0.1\","
+ + "\"gitRepoUrl\":\"https://github.com/apify/x.git\"}}",
+ Build.class);
+ assertEquals("latest", tagged.getActVersion().getBuildTag());
+ assertEquals("0.1", tagged.getActVersion().getVersionNumber());
+ assertEquals("GIT_REPO", tagged.getActVersion().getSourceType());
+ assertEquals("https://github.com/apify/x.git", tagged.getActVersion().getGitRepoUrl());
+ assertTrue(tagged.getActVersion().getSourceFiles().isEmpty());
+
+ Build untagged =
+ parse(
+ "{\"id\":\"b2\",\"actVersion\":{\"sourceType\":\"SOURCE_FILES\","
+ + "\"buildTag\":null,\"versionNumber\":\"0.0\",\"sourceFiles\":["
+ + "{\"name\":\"main.js\",\"format\":\"TEXT\",\"content\":\"x\"}]}}",
+ Build.class);
+ assertNull(untagged.getActVersion().getBuildTag());
+ assertEquals(1, untagged.getActVersion().getSourceFiles().size());
+ assertEquals("main.js", untagged.getActVersion().getSourceFiles().get(0).getName());
+ assertEquals("TEXT", untagged.getActVersion().getSourceFiles().get(0).getFormat());
+ assertEquals("x", untagged.getActVersion().getSourceFiles().get(0).getContent());
+ assertEquals("SOURCE_FILES", untagged.getActVersion().getSourceType());
+ }
+
+ @Test
+ void webhookModelsAdHocFlagAndCondition() {
+ Webhook adHoc =
+ parse(
+ "{\"id\":\"w1\",\"isAdHoc\":true,\"condition\":{\"actorRunId\":\"run1\"}}",
+ Webhook.class);
+ assertTrue(adHoc.isAdHoc());
+ assertEquals("run1", adHoc.getCondition().get("actorRunId").asString());
+
+ Webhook persistent =
+ parse(
+ "{\"id\":\"w2\",\"isAdHoc\":false,\"condition\":{\"actorTaskId\":\"t1\"}}",
+ Webhook.class);
+ assertEquals("t1", persistent.getCondition().get("actorTaskId").asString());
+ }
+
/**
* Every other nested value DTO in the audit (one representative per resource package) also
* preserves fields it has no typed getter for, instead of discarding them — the general fix, not
diff --git a/src/test/java/com/apify/client/integration/IterationIntegrationTest.java b/src/test/java/com/apify/client/integration/IterationIntegrationTest.java
index 1d472aa..2270130 100644
--- a/src/test/java/com/apify/client/integration/IterationIntegrationTest.java
+++ b/src/test/java/com/apify/client/integration/IterationIntegrationTest.java
@@ -300,12 +300,15 @@ void iterateSchedules() {
void iterateWebhooks() {
ApifyClient client = requireClient();
Set ids = new HashSet<>();
+ Task task = WebhookIntegrationTest.createOwnedTask(client);
try {
for (int i = 0; i < 2; i++) {
ids.add(
client
.webhooks()
- .create(WebhookIntegrationTest.webhookDef("https://example.com/it-wh-" + i))
+ .create(
+ WebhookIntegrationTest.webhookDef(
+ "https://example.com/it-wh-" + i, task.getId()))
.join()
.getId());
}
@@ -315,8 +318,12 @@ void iterateWebhooks() {
Webhook::getId,
ids));
} finally {
- for (String id : ids) {
- client.webhook(id).delete().join();
+ try {
+ for (String id : ids) {
+ client.webhook(id).delete().join();
+ }
+ } finally {
+ client.task(task.getId()).delete().join();
}
}
}
diff --git a/src/test/java/com/apify/client/integration/WebhookIntegrationTest.java b/src/test/java/com/apify/client/integration/WebhookIntegrationTest.java
index a8ca400..f2b6b55 100644
--- a/src/test/java/com/apify/client/integration/WebhookIntegrationTest.java
+++ b/src/test/java/com/apify/client/integration/WebhookIntegrationTest.java
@@ -5,6 +5,7 @@
import com.apify.client.ApifyClient;
import com.apify.client.ListOptions;
+import com.apify.client.task.Task;
import com.apify.client.webhook.Webhook;
import com.apify.client.webhook.WebhookClient;
import com.apify.client.webhook.WebhookDispatch;
@@ -14,18 +15,29 @@
class WebhookIntegrationTest extends IntegrationBase {
- static Map webhookDef(String url) {
+ /**
+ * A persistent (non-ad-hoc) webhook definition conditioned on the given task. The API rejects a
+ * condition that targets a resource that does not exist, and ad hoc webhooks can only target
+ * Actor runs. A webhook on a shared Actor would fire on other suites' concurrent runs, so each
+ * test conditions its webhook on a task it owns.
+ */
+ static Map webhookDef(String url, String taskId) {
return Map.of(
"isAdHoc",
- true,
+ false,
"eventTypes",
List.of("ACTOR.RUN.SUCCEEDED"),
"condition",
- Map.of("actorRunId", "ZZZZZZZZZZZZZZZZZ"),
+ Map.of("actorTaskId", taskId),
"requestUrl",
url);
}
+ /** Creates a uniquely named task owned by the calling test; the caller deletes it. */
+ static Task createOwnedTask(ApifyClient client) {
+ return client.tasks().create(TaskIntegrationTest.taskDef(uniqueName("wh-task"))).join();
+ }
+
@Test
void listWebhooks() {
ApifyClient client = requireClient();
@@ -41,67 +53,86 @@ void listWebhookDispatches() {
@Test
void getWebhook() {
ApifyClient client = requireClient();
- Webhook wh = client.webhooks().create(webhookDef("https://example.com/webhook")).join();
+ Task task = createOwnedTask(client);
try {
- var got = client.webhook(wh.getId()).get().join();
- assertTrue(got.isPresent());
- assertEquals(wh.getId(), got.get().getId());
+ Webhook wh =
+ client.webhooks().create(webhookDef("https://example.com/webhook", task.getId())).join();
+ try {
+ var got = client.webhook(wh.getId()).get().join();
+ assertTrue(got.isPresent());
+ assertEquals(wh.getId(), got.get().getId());
+ } finally {
+ client.webhook(wh.getId()).delete().join();
+ }
} finally {
- client.webhook(wh.getId()).delete().join();
+ client.task(task.getId()).delete().join();
}
}
@Test
void getWebhookDispatch() {
ApifyClient client = requireClient();
- Webhook wh = client.webhooks().create(webhookDef("https://example.com/webhook")).join();
+ Task task = createOwnedTask(client);
try {
- WebhookDispatch dispatch = client.webhook(wh.getId()).test().join();
- var got = client.webhookDispatch(dispatch.getId()).get().join();
- assertTrue(got.isPresent());
- assertEquals(dispatch.getId(), got.get().getId());
+ Webhook wh =
+ client.webhooks().create(webhookDef("https://example.com/webhook", task.getId())).join();
+ try {
+ WebhookDispatch dispatch = client.webhook(wh.getId()).test().join();
+ var got = client.webhookDispatch(dispatch.getId()).get().join();
+ assertTrue(got.isPresent());
+ assertEquals(dispatch.getId(), got.get().getId());
+ } finally {
+ client.webhook(wh.getId()).delete().join();
+ }
} finally {
- client.webhook(wh.getId()).delete().join();
+ client.task(task.getId()).delete().join();
}
}
@Test
void webhookCrudFlow() {
ApifyClient client = requireClient();
- Webhook wh = client.webhooks().create(webhookDef("https://example.com/webhook")).join();
+ Task task = createOwnedTask(client);
try {
- WebhookClient webhook = client.webhook(wh.getId());
- assertTrue(webhook.get().join().isPresent());
- Webhook updated = webhook.update(Map.of("requestUrl", "https://example.com/updated")).join();
- assertEquals("https://example.com/updated", updated.getRequestUrl());
- webhook.dispatches().list(new ListOptions()).join();
- webhook.test().join();
+ Webhook wh =
+ client.webhooks().create(webhookDef("https://example.com/webhook", task.getId())).join();
+ try {
+ WebhookClient webhook = client.webhook(wh.getId());
+ assertTrue(webhook.get().join().isPresent());
+ Webhook updated =
+ webhook.update(Map.of("requestUrl", "https://example.com/updated")).join();
+ assertEquals("https://example.com/updated", updated.getRequestUrl());
+ webhook.dispatches().list(new ListOptions()).join();
+ webhook.test().join();
- // list() step of the create/get/modify/list/delete flow: verify the just-created webhook
- // appears in the top-level collection listing.
- boolean foundInList =
- pollUntil(
- LIST_FIND_ATTEMPTS,
- LIST_FIND_BACKOFF_MILLIS,
- () ->
- client
- .webhooks()
- .list(new ListOptions().desc(true).limit(10L))
- .join()
- .getItems()
- .stream()
- .anyMatch(w -> wh.getId().equals(w.getId())));
- assertTrue(foundInList, "expected the just-created webhook to appear in the top-level list");
+ // list() step of the create/get/modify/list/delete flow: verify the just-created webhook
+ // appears in the top-level collection listing.
+ boolean foundInList =
+ pollUntil(
+ LIST_FIND_ATTEMPTS,
+ LIST_FIND_BACKOFF_MILLIS,
+ () ->
+ client
+ .webhooks()
+ .list(new ListOptions().desc(true).limit(10L))
+ .join()
+ .getItems()
+ .stream()
+ .anyMatch(w -> wh.getId().equals(w.getId())));
+ assertTrue(
+ foundInList, "expected the just-created webhook to appear in the top-level list");
- // Typed getters (previously only reachable via getExtra()): verify the API's response
- // actually deserializes into them, not just that the code compiles.
- assertTrue(wh.isAdHoc());
- assertTrue(wh.getCondition() != null);
- assertTrue(wh.getCreatedAt() != null);
- assertTrue(wh.getModifiedAt() != null);
- assertTrue(wh.getStats() != null);
+ // Typed getters (previously only reachable via getExtra()): verify the API's response
+ // actually deserializes into them, not just that the code compiles.
+ assertEquals(task.getId(), wh.getCondition().get("actorTaskId").asString());
+ assertTrue(wh.getCreatedAt() != null);
+ assertTrue(wh.getModifiedAt() != null);
+ assertTrue(wh.getStats() != null);
+ } finally {
+ client.webhook(wh.getId()).delete().join();
+ }
} finally {
- client.webhook(wh.getId()).delete().join();
+ client.task(task.getId()).delete().join();
}
}
}