Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ All notable changes to the Rust Apify API client are documented here. The format
based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres
to [Semantic Versioning](https://semver.org/).

## [0.10.2] - 2026-09-25

### Changed
- Bumped `API_SPEC_VERSION` to `v2-2026-09-24T114302Z` (the `idempotency-key` header on the
charge endpoint is now documented as required and expiring 3 minutes after the charge, and
the task-publish description now states the 10-per-Actor / 100-per-account publication
limits; the client already always sends the idempotency key, so only doc comments changed).
- Documented the task-publish limits (10 per Actor, 100 per account) on `TaskClient::publish`
and the idempotency-key expiration window on `RunChargeOptions::idempotency_key`.
- Bumped crate version to `0.10.2`.

## [0.10.1] - 2026-09-11

### Changed
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "apify-client"
version = "0.10.1"
version = "0.10.2"
authors = ["Apify Technologies <support@apify.com>"]
description = "An official, but experimental, AI-generated and AI-maintained Rust client for the Apify API (https://apify.com)."
license = "Apache-2.0"
Expand Down
4 changes: 3 additions & 1 deletion docs/runs.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ collections are available via `actor.runs()` and `task.runs()`.

`RunMetamorphOptions`: `build`, `content_type` (both optional; `content_type` defaults to `application/json`).

`RunChargeOptions`: `event_name` (required), `count` (defaults to `1`), `idempotency_key` (auto-generated when omitted).
`RunChargeOptions`: `event_name` (required), `count` (defaults to `1`), `idempotency_key`
(auto-generated when omitted; the API requires this header and forgets the key 3 minutes after
the charge, so a retry past that window creates a new charge instead of being deduplicated).

`ActorRun.status` is a stringly-typed `Option<String>` carrying the API's run status. Known
values are `READY`, `RUNNING`, `SUCCEEDED`, `FAILED`, `ABORTING`, `ABORTED`, `TIMING-OUT`, and
Expand Down
2 changes: 1 addition & 1 deletion docs/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Obtained via `client.tasks()` (collection) and `client.task(id)` (single).
|---|---|---|---|
| `get()` | — | `Option<Task>` | Fetches the task. |
| `update(fields)` | `&impl Serialize` | `Task` | Updates the task. |
| `publish()` | — | `Task` | Publishes the task on its public landing page (sets `isPublic: true`). Requires the task's Actor to be public and the task to have `publicConfig` set up. |
| `publish()` | — | `Task` | Publishes the task on its public landing page (sets `isPublic: true`). Requires the task's Actor to be public and the task to have `publicConfig` set up. An Actor may have at most 10 published tasks, an account at most 100 (contact Apify support to raise either limit). |
| `unpublish()` | — | `Task` | Unpublishes the task from its public landing page (sets `isPublic: false`), preserving `publicConfig`. |
| `delete()` | — | `()` | Deletes the task. |
| `start(input, options)` | `Option<&impl Serialize>`, `ActorStartOptions` | `ActorRun` | Starts a run. See [`ActorStartOptions`](actors.md#actorstartoptions) for the full field list. |
Expand Down
8 changes: 5 additions & 3 deletions src/clients/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,11 @@ pub struct RunChargeOptions {
pub event_name: String,
/// Number of times to charge the event (defaults to `1`).
pub count: Option<i64>,
/// Idempotency key deduplicating the charge across retries. If `None`, one is
/// auto-generated as `{runId}-{eventName}-{timestampMillis}-{random}`, matching the
/// reference client, so a transport-retried charge is applied at most once.
/// Idempotency key deduplicating the charge across retries. The API requires this header
/// on every charge request and forgets each key 3 minutes after the charge, so a retry sent
/// past that window with the same key creates a new charge rather than being deduplicated.
/// If `None`, one is auto-generated as `{runId}-{eventName}-{timestampMillis}-{random}`,
/// matching the reference client, so a transport-retried charge is applied at most once.
pub idempotency_key: Option<String>,
}

Expand Down
6 changes: 4 additions & 2 deletions src/clients/task.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,10 @@ impl TaskClient {
/// [`TaskClient::update`].
///
/// The task's Actor must be public and the task must already have its public display
/// configuration ([`Task::public_config`]) set up. Publishing an already-published task
/// does nothing. The returned [`Task::is_public`] reflects the new publication state.
/// configuration ([`Task::public_config`]) set up. An Actor can have at most 10 published
/// tasks, and an account at most 100 across all its Actors; contact Apify support to raise
/// either limit. Publishing an already-published task does nothing. The returned
/// [`Task::is_public`] reflects the new publication state.
pub async fn publish(&self) -> ApifyClientResult<Task> {
self.update(&serde_json::json!({ "isPublic": true })).await
}
Expand Down
2 changes: 1 addition & 1 deletion src/version.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ pub const CLIENT_VERSION: &str = env!("CARGO_PKG_VERSION");
/// and verified against.
///
/// This corresponds to the `info.version` field of the Apify OpenAPI document.
pub const API_SPEC_VERSION: &str = "v2-2026-09-10T091137Z";
pub const API_SPEC_VERSION: &str = "v2-2026-09-24T114302Z";
55 changes: 55 additions & 0 deletions tests/unit_http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,61 @@ async fn iterate_keys_zero_limit_sends_no_limit_and_walks_all() {
);
}

/// `RunClient::charge` always sends the `idempotency-key` header (required by the spec) and the
/// `eventName`/`count` body, whether or not an explicit key is supplied. This is hermetic because
/// a real charge can only be made from inside a running pay-per-event Actor with that run's own
/// token, which the integration suite (running as an external account-level client) cannot do.
#[tokio::test]
async fn charge_sends_required_idempotency_key_header() {
let backend = MockBackend::new(vec![MockOutcome::Status(201, b"{}".to_vec())]);
let client = client_with(backend.clone(), 0);

// No explicit key -> one is still auto-generated and sent.
client
.run("some-run-id")
.charge(apify_client::RunChargeOptions {
event_name: "ANALYZE_PAGE".to_owned(),
count: Some(2),
idempotency_key: None,
})
.await
.expect("charge should succeed");
assert!(
backend
.last_header("idempotency-key")
.is_some_and(|k| !k.is_empty()),
"an idempotency-key header must always be sent, even when none is supplied"
);
let url = backend.last_url().expect("a request was sent");
assert!(
url.contains("/actor-runs/some-run-id/charge"),
"charge must POST to the run's /charge endpoint, got {url}"
);
let body = backend.last_body().expect("a body was sent");
let parsed: serde_json::Value = serde_json::from_slice(&body).expect("valid JSON body");
assert_eq!(parsed["eventName"], "ANALYZE_PAGE");
assert_eq!(parsed["count"], 2);

// Explicit key -> sent verbatim (not overwritten by an auto-generated one).
client
.run("some-run-id")
.charge(apify_client::RunChargeOptions {
event_name: "ANALYZE_PAGE".to_owned(),
count: None,
idempotency_key: Some("my-explicit-key".to_owned()),
})
.await
.expect("charge should succeed");
assert_eq!(
backend.last_header("idempotency-key").as_deref(),
Some("my-explicit-key"),
"an explicit idempotency key must be sent verbatim"
);
let body = backend.last_body().expect("a body was sent");
let parsed: serde_json::Value = serde_json::from_slice(&body).expect("valid JSON body");
assert_eq!(parsed["count"], 1, "count defaults to 1 when omitted");
}

/// A finite `iterate_keys` cap larger than one page must be clamped to the endpoint maximum per
/// request (so it does not 400) while still yielding across pages until the cap or the store is
/// exhausted.
Expand Down
Loading