Skip to content
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,47 @@ All notable changes to the Apify Go 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.6.0] - 2026-07-10

### Added

- Lazy `Iterate` helpers on every list collection (`Actors`, `Runs`, `Builds`, `Tasks`,
`Datasets`, `KeyValueStores`, `RequestQueues`, `Schedules`, `Webhooks`, `WebhookDispatches`,
actor versions and env vars) plus dataset-item iteration (`DatasetClient.IterateItems` and the
generic `IterateDatasetItems[T]`), backed by a new exported generic iterator type
`ListIterator[T]`. As in the reference client's iterable `list()`, the options' `Limit` caps the
total number of items yielded across all pages (unset means all), and the per-page size is a
separate `chunkSize` argument (nil for the server default).
- Cursor-based key iteration on the key-value store: `KeyValueStoreClient.IterateKeys` returns a
`KeyValueStoreKeysIterator` that lazily walks all keys via `nextExclusiveStartKey`, matching the
reference client's iterable `listKeys()`. `Limit` caps the total keys yielded and `chunkSize` is
the page size.

### Fixed

- The `Iterate` helpers now honor a caller-set `Offset` on the list options as the starting point
(iteration begins there and the cap counts from that offset), instead of silently discarding it.
Matches the reference client's `options.offset` handling.

### Changed

- Bumped `APISpecVersion` to `v2-2026-07-10T105921Z`.
- Bumped `ClientVersion` to `0.6.0`.
- **Breaking:** `StoreCollectionClient.Iterate` now takes a second `chunkSize *int64` argument and
treats the options' `Limit` as a total-item cap rather than the per-page size, to match the
reference client's iterator semantics. `StoreActorIterator` is now an alias of
`ListIterator[ActorStoreListItem]`.
- Synced the `APISpecVersion` reference in the `README.md` "Versioning" section to match `version.go`.

### Documentation

- Documented how the client-side `Call`/`WaitForFinish` polling relates to `WithTimeout` (each
poll asks the server to wait ≤60s, so the per-request timeout never cuts off a `nil` wait).
- Added a pointer to where API tokens come from (Apify Console → Settings → Integrations).
- Added the `IterateDatasetItems[T]` signature and a usage example to the storages guide.
- Added `WithPublicBaseURL` to the `NewClientWithOptions` sample and a note distinguishing
`client.Build(id)` from `Actor.Build(...)`.

## [0.5.0] - 2026-07-09

### Changed
Expand Down
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,13 @@ import (
"context"
"fmt"
"log"
"os"

apify "github.com/apify/apify-client-go"
)

func main() {
client := apify.NewClient("my-api-token")
client := apify.NewClient(os.Getenv("APIFY_TOKEN"))
ctx := context.Background()

// Start an Actor and wait for it to finish.
Expand All @@ -63,6 +64,11 @@ func main() {
}
```

Get your API token from the [Apify Console](https://console.apify.com/) under **Settings →
Integrations** (the **Personal API tokens** section). The client never reads it from the
environment itself: pass the token to `NewClient`/`WithToken` explicitly (the examples above
read `APIFY_TOKEN` from the environment only as a convenience in `main`).

## Configuration

Use `NewClient(token)` for a token-only setup, or `NewClientWithOptions` with functional
Expand All @@ -71,8 +77,9 @@ options for full control:
```go
client := apify.NewClientWithOptions(
apify.WithToken("my-api-token"),
apify.WithBaseURL("https://api.apify.com"), // /v2 is appended automatically
apify.WithMaxRetries(8), // default 8
apify.WithBaseURL("https://api.apify.com"), // /v2 is appended automatically
apify.WithPublicBaseURL("https://api.apify.com"), // base for signed, shareable URLs
apify.WithMaxRetries(8), // default 8
apify.WithMinDelayBetweenRetries(500*time.Millisecond),
apify.WithTimeout(360*time.Second), // default 6 minutes
apify.WithUserAgentSuffix("MyTool/1.0"),
Expand Down Expand Up @@ -179,7 +186,7 @@ func main() {

- `apify.ClientVersion` — the semantic version of this library.
- `apify.APISpecVersion` — the Apify OpenAPI spec version this client was built against
(`v2-2026-07-08T143931Z`).
(`v2-2026-07-10T105921Z`).

### Releasing

Expand Down
13 changes: 13 additions & 0 deletions actor_collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,19 @@ func (c *ActorCollectionClient) List(ctx context.Context, options ActorListOptio
return listResource[Actor](ctx, c.ctx, "", params)
}

// Iterate returns a lazy iterator over the Actors matching the options, fetching pages on
// demand. The options' Limit caps the total number of Actors yielded (unset means all); the
// per-page size is chunkSize (nil for the server default). Mirrors the reference client's
// iterable list().
func (c *ActorCollectionClient) Iterate(options ActorListOptions, chunkSize *int64) *ListIterator[Actor] {
return newListIterator(options.Limit, chunkSize, offsetVal(options.Offset), func(ctx context.Context, offset, limit int64) (PaginationList[Actor], error) {
opts := options
opts.Offset = &offset
opts.Limit = pageLimitPtr(limit)
return c.List(ctx, opts)
})
}

// Create creates a new Actor. actor is any JSON-serializable Actor definition.
func (c *ActorCollectionClient) Create(ctx context.Context, actor any) (Actor, error) {
return createResource[Actor](ctx, c.ctx, NewQueryParams(), actor)
Expand Down
10 changes: 10 additions & 0 deletions actor_env_var.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ func (c *ActorEnvVarCollectionClient) List(ctx context.Context) (PaginationList[
return listResource[ActorEnvVar](ctx, c.ctx, "", NewQueryParams())
}

// Iterate returns a lazy iterator over the version's environment variables. Mirrors the
// reference client's iterable list(). The env-vars endpoint is not offset-paginated (it
// returns the full set in a single page), so there is no Limit/chunkSize control and the
// closure ignores the offset/limit arguments; the iterator drains that one page.
func (c *ActorEnvVarCollectionClient) Iterate() *ListIterator[ActorEnvVar] {
return newListIterator(nil, nil, 0, func(ctx context.Context, _, _ int64) (PaginationList[ActorEnvVar], error) {
return c.List(ctx)
})
}

// Create creates a new environment variable.
func (c *ActorEnvVarCollectionClient) Create(ctx context.Context, envVar ActorEnvVar) (ActorEnvVar, error) {
return createResource[ActorEnvVar](ctx, c.ctx, NewQueryParams(), envVar)
Expand Down
13 changes: 13 additions & 0 deletions actor_version.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,19 @@ func (c *ActorVersionCollectionClient) List(ctx context.Context, options ListOpt
return listResource[ActorVersion](ctx, c.ctx, "", params)
}

// Iterate returns a lazy iterator over the Actor's versions matching the options, fetching
// pages on demand. The options' Limit caps the total number of versions yielded (unset means
// all); the per-page size is chunkSize (nil for the server default). Mirrors the reference
// client's iterable list().
func (c *ActorVersionCollectionClient) Iterate(options ListOptions, chunkSize *int64) *ListIterator[ActorVersion] {
return newListIterator(options.Limit, chunkSize, offsetVal(options.Offset), func(ctx context.Context, offset, limit int64) (PaginationList[ActorVersion], error) {
opts := options
opts.Offset = &offset
opts.Limit = pageLimitPtr(limit)
return c.List(ctx, opts)
})
}

// Create creates a new Actor version. version is any JSON-serializable version definition.
func (c *ActorVersionCollectionClient) Create(ctx context.Context, version any) (ActorVersion, error) {
return createResource[ActorVersion](ctx, c.ctx, NewQueryParams(), version)
Expand Down
13 changes: 13 additions & 0 deletions build.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,19 @@ func (c *BuildCollectionClient) List(ctx context.Context, options ListOptions) (
return listResource[Build](ctx, c.ctx, "", params)
}

// Iterate returns a lazy iterator over the builds matching the options, fetching pages on
// demand. The options' Limit caps the total number of builds yielded (unset means all); the
// per-page size is chunkSize (nil for the server default). Mirrors the reference client's
// iterable list().
func (c *BuildCollectionClient) Iterate(options ListOptions, chunkSize *int64) *ListIterator[Build] {
return newListIterator(options.Limit, chunkSize, offsetVal(options.Offset), func(ctx context.Context, offset, limit int64) (PaginationList[Build], error) {
opts := options
opts.Offset = &offset
opts.Limit = pageLimitPtr(limit)
return c.List(ctx, opts)
})
}

// BuildClient is a client for a specific Actor build (/v2/actor-builds/{buildId}).
type BuildClient struct {
ctx *resourceContext
Expand Down
33 changes: 31 additions & 2 deletions dataset.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,11 +152,12 @@ func (c *DatasetClient) Delete(ctx context.Context) error {
return deleteResource(ctx, c.ctx, "")
}

// ListItems lists items from the dataset.
// ListDatasetItems lists a single page of items from the dataset, decoding each into T
// (e.g. json.RawMessage or a struct).
//
// The dataset items endpoint returns a bare JSON array (not a data envelope) and reports
// pagination via X-Apify-Pagination-* headers, which are surfaced in the returned
// [PaginationList]. T is the item type to decode into (e.g. json.RawMessage or a struct).
// [PaginationList].
func ListDatasetItems[T any](ctx context.Context, c *DatasetClient, options DatasetListItemsOptions) (PaginationList[T], error) {
var result PaginationList[T]
params := NewQueryParams()
Expand Down Expand Up @@ -188,6 +189,34 @@ func (c *DatasetClient) ListItems(ctx context.Context, options DatasetListItemsO
return ListDatasetItems[json.RawMessage](ctx, c, options)
}

// IterateDatasetItems returns a lazy iterator over the dataset's items, decoding each into T
// and fetching pages on demand. The options' Limit caps the total number of items yielded
// (unset means all); the per-page size is chunkSize (nil for the server default). Mirrors the
// reference client's iterable listItems().
//
// Caveat: offset-based iteration paginates using the item total reported in the
// X-Apify-Pagination-Total header, and that header can lag right after items are pushed (the
// count is updated asynchronously). Iterating immediately after a push may therefore stop early
// (after one page) until the total settles. This matches the reference client's behaviour; wait
// for the total to converge before iterating a just-written dataset if completeness matters.
func IterateDatasetItems[T any](c *DatasetClient, options DatasetListItemsOptions, chunkSize *int64) *ListIterator[T] {
return newListIterator(options.Limit, chunkSize, offsetVal(options.Offset), func(ctx context.Context, offset, limit int64) (PaginationList[T], error) {
opts := options
opts.Offset = &offset
opts.Limit = pageLimitPtr(limit)
return ListDatasetItems[T](ctx, c, opts)
})
}

// IterateItems returns a lazy iterator over the dataset's items, decoding each into a generic
// json.RawMessage. For typed decoding use [IterateDatasetItems]. See IterateDatasetItems for
// how the options' Limit (total cap) and chunkSize (page size) are interpreted, including the
// caveat that the pagination-total header can lag right after a push and cause an immediate
// iteration to stop after one page.
func (c *DatasetClient) IterateItems(options DatasetListItemsOptions, chunkSize *int64) *ListIterator[json.RawMessage] {
return IterateDatasetItems[json.RawMessage](c, options, chunkSize)
}

// DownloadItems downloads dataset items serialized in the given format, returning the raw
// bytes. Unlike ListItems (parsed items), this returns the items already serialized to JSON,
// CSV, XLSX, XML, RSS or HTML — useful for exporting.
Expand Down
13 changes: 13 additions & 0 deletions dataset_collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,19 @@ func (c *DatasetCollectionClient) List(ctx context.Context, options StorageListO
return listResource[Dataset](ctx, c.ctx, "", params)
}

// Iterate returns a lazy iterator over the datasets matching the options, fetching pages on
// demand. The options' Limit caps the total number of datasets yielded (unset means all); the
// per-page size is chunkSize (nil for the server default). Mirrors the reference client's
// iterable list().
func (c *DatasetCollectionClient) Iterate(options StorageListOptions, chunkSize *int64) *ListIterator[Dataset] {
return newListIterator(options.Limit, chunkSize, offsetVal(options.Offset), func(ctx context.Context, offset, limit int64) (PaginationList[Dataset], error) {
opts := options
opts.Offset = &offset
opts.Limit = pageLimitPtr(limit)
return c.List(ctx, opts)
})
}

// GetOrCreate gets the dataset with the given name, creating it if it does not exist. An
// empty name creates a new unnamed dataset.
func (c *DatasetCollectionClient) GetOrCreate(ctx context.Context, name string) (Dataset, error) {
Expand Down
22 changes: 20 additions & 2 deletions docs/actors.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Actors are the programs that run on the Apify platform. Access the Actor collect
| Method | Description |
| --- | --- |
| `List(ctx, ActorListOptions) (PaginationList[Actor], error)` | List the account's Actors. |
| `Iterate(ActorListOptions, chunkSize *int64) *ListIterator[Actor]` | Lazy iterator over matching Actors. `Limit` caps the total yielded; `chunkSize` is the page size. |
| `Create(ctx, definition any) (Actor, error)` | Create a new Actor. |

`Create` takes a free-form definition (`any`) serialized to JSON, so the Actor's fields are
Expand Down Expand Up @@ -92,6 +93,11 @@ The `Actor` value returned by `Get`/`Create`/`Update` and listed by `List`:
| `Version(n) *ActorVersionClient` / `Versions() *ActorVersionCollectionClient` | Versions. |
| `Webhooks() *WebhookCollectionClient` | This Actor's webhooks. |

> **Note — two different `Build`s.** `Actor.Build(ctx, versionNumber, ActorBuildOptions)` here
> *starts* a build of a version and returns the resulting `Build`. It is unrelated to the
> top-level accessor `client.Build(id)`, which returns a `*BuildClient` for inspecting an
> existing build by ID (see [builds.md](builds.md)). Same name, different jobs.

`ValidateInput` is equivalent to `ValidateInputForBuild(ctx, input, "")`: an empty `build`
omits the parameter, so the API validates against the build tagged `latest` (per the API
specification). Both return the raw JSON validation result from the API — a JSON object
Expand Down Expand Up @@ -167,15 +173,27 @@ run, err := client.Actor("apify/hello-world").Call(ctx,
)
```

> **How `Call`/`WaitForFinish` relate to `WithTimeout`.** The wait is done client-side by
> *polling*: the client repeatedly re-fetches the run, each poll being a separate HTTP request
> that asks the server to block for at most 60 seconds (the API's per-request wait cap). Because
> every poll returns within that 60-second server cap — comfortably inside the client's
> `WithTimeout` budget (default 360s, which bounds each individual request, not the total wait) —
> the overall wait is **not** cut off at 360s. It continues across as many polls as needed until
> the run reaches a terminal state. Passing `waitSecs == nil` therefore genuinely waits until the
> run finishes (bounded only by a very large internal cap of ~11.5 days, or by cancelling the
> `ctx` you pass in); a non-nil `waitSecs` bounds the total client-side wait instead. The distinct
> `ActorStartOptions.WaitForFinish` field is unrelated: it only controls the single server-side
> wait on the initial `Start` request (max 60s), not the client-side polling loop.

## Versions and environment variables

`client.Actor(id).Versions()` and `.Version(n)`:

| Method | Description |
| --- | --- |
| `Versions().List(ctx, ListOptions)` / `Versions().Create(ctx, def)` | List/create versions. |
| `Versions().List(ctx, ListOptions)` / `Versions().Iterate(ListOptions, chunkSize *int64)` / `Versions().Create(ctx, def)` | List/iterate/create versions. |
| `Version(n).Get/Update/Delete(ctx)` | Manage a single version. |
| `Version(n).EnvVars().List(ctx)` / `.Create(ctx, ActorEnvVar)` | List/create env vars. |
| `Version(n).EnvVars().List(ctx)` / `.Iterate()` / `.Create(ctx, ActorEnvVar)` | List/iterate/create env vars. |
| `Version(n).EnvVar(name).Get/Update/Delete(ctx)` | Manage a single env var. |

`ActorEnvVar` fields:
Expand Down
1 change: 1 addition & 0 deletions docs/builds.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ single build with `client.Build(id)`, and an Actor's builds with `client.Actor(i
| Method | Description |
| --- | --- |
| `List(ctx, ListOptions) (PaginationList[Build], error)` | List builds. |
| `Iterate(ListOptions, chunkSize *int64) *ListIterator[Build]` | Lazy iterator over matching builds. `Limit` caps the total yielded; `chunkSize` is the page size. |

## Single build

Expand Down
10 changes: 7 additions & 3 deletions docs/misc.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Browse public Actors with `client.Store()`:
| Method | Description |
| --- | --- |
| `List(ctx, StoreListOptions) (PaginationList[ActorStoreListItem], error)` | One page of Store Actors. |
| `Iterate(StoreListOptions) *StoreActorIterator` | Lazy iterator over all matching Actors. |
| `Iterate(StoreListOptions, chunkSize *int64) *StoreActorIterator` | Lazy iterator over matching Actors. `Limit` caps the total yielded; `chunkSize` is the page size. |

`StoreListOptions` (all fields optional):

Expand Down Expand Up @@ -35,7 +35,8 @@ Each item is an `ActorStoreListItem`:
| `Extra` | `map[string]json.RawMessage` | Any other fields returned by the API (forward compatibility). |

```go
it := client.Store().Iterate(apify.StoreListOptions{Limit: apify.Ptr(int64(20)), Search: apify.Ptr("scraper")})
// Limit caps the total number of Actors yielded; the second argument is the per-page size.
it := client.Store().Iterate(apify.StoreListOptions{Limit: apify.Ptr(int64(20)), Search: apify.Ptr("scraper")}, apify.Ptr(int64(10)))
for {
actor, err := it.Next(ctx)
if err != nil {
Expand Down Expand Up @@ -81,7 +82,10 @@ user, ok, err := client.Me().Get(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Println(user.Username, ok)
if !ok {
log.Fatal("account not found")
}
fmt.Println(user.Username)

usage, err := client.Me().MonthlyUsage(ctx)
if err != nil {
Expand Down
5 changes: 4 additions & 1 deletion docs/runs.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ with `client.Run(id)`, and an Actor's or task's runs with `client.Actor(id).Runs
| Method | Description |
| --- | --- |
| `List(ctx, ListOptions, RunListOptions) (PaginationList[ActorRun], error)` | List runs. |
| `Iterate(ListOptions, RunListOptions, chunkSize *int64) *ListIterator[ActorRun]` | Lazy iterator over matching runs. `Limit` caps the total yielded; `chunkSize` is the page size. |

`RunListOptions`:

Expand Down Expand Up @@ -72,7 +73,9 @@ The time filters apply only to Actor- and task-scoped collections.
> (empty string) as "unset"; only `RunChargeOptions.Count` is a pointer.

```go
run, err := client.Run(runID).WaitForFinish(ctx, nil) // nil waits indefinitely
// nil polls until the run is terminal and is not cut off by WithTimeout (which bounds each
// poll request, not the total wait) — see docs/actors.md for the full explanation.
run, err := client.Run(runID).WaitForFinish(ctx, nil)
if err != nil {
log.Fatal(err)
}
Expand Down
1 change: 1 addition & 0 deletions docs/schedules.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Schedules start Actor or task runs at specified times. Access the schedule colle
| Method | Description |
| --- | --- |
| `List(ctx, ListOptions) (PaginationList[Schedule], error)` | List the account's schedules. |
| `Iterate(ListOptions, chunkSize *int64) *ListIterator[Schedule]` | Lazy iterator over matching schedules. `Limit` caps the total yielded; `chunkSize` is the page size. |
| `Create(ctx, definition any) (Schedule, error)` | Create a new schedule. |

### `Schedule` fields
Expand Down
Loading
Loading