# Website Screenshot Studio (`aster_data/website-screenshot-studio`) Actor

Capture websites as PNG, JPEG, or PDF on desktop, tablet, and mobile. Automatically dismiss consent dialogs and popups, with bounded lazy loading, explicit errors, and an HTML gallery.

- **URL**: https://apify.com/aster_data/website-screenshot-studio.md
- **Developed by:** [Falak Fatma](https://apify.com/aster_data) (community)
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 successful captures

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Website Screenshot Studio

Capture batches of websites as **PNG, JPEG, or PDF** on desktop, tablet, and mobile. Every unique URL × device produces a dataset row, including failed captures. Images and PDFs are stored in the run's key-value store, alongside a gallery and run summary.

### Pricing

The capture fee is **$0.002 per successful file ($2 per 1,000)**. PNG, JPEG, full-page, element, and PDF captures use the same fee. One URL captured on three devices produces three billable files. A multipage PDF counts as one file. Retries do not add capture fees, and failed captures or HTTP-error diagnostic images have no capture fee.

Apify's automatic startup event is **$0.00005 per allocated GB, with a minimum of one event**. At 4 GB that is **$0.0002 per run**. The [live Pricing tab](https://apify.com/aster_data/website-screenshot-studio/pricing) shows whether compute, storage, transfer, and optional proxy costs are included or billed separately. If metered separately, those costs and the startup event can still apply when a capture fails.

Set **Max cost per run** in Apify to limit spending. The Actor stops starting captures when the remaining event budget cannot fund another successful file; skipped jobs receive `BUDGET_EXCEEDED` rows without artifacts. Native spending limits may stop a run earlier, leaving partial results.

This is an independent implementation inspired by [Apify's Website Screenshot Generator](https://apify.com/apify/screenshot-url). The [reference audit](docs/AUDIT.md) explains the design choices and tradeoffs.

### Quick start locally

Requires Node.js 22 or newer.

```sh
npm ci
npm run browsers:install
npm run demo
npm run preview
```

Open **http://127.0.0.1:4321**. The demo uses a local fixture with desktop and mobile pages, a redirect, a lazy image, and an intentional HTTP 404. It needs no account or API token. Four of its six captures should succeed; the other two show error handling.

To capture a real website:

```sh
npm start -- --input examples/basic.json
npm run preview
```

Use a fresh storage directory to preserve previous results:

```sh
CRAWLEE_STORAGE_DIR=./storage/my-run npm start -- --input examples/mobile.json
CRAWLEE_STORAGE_DIR=./storage/my-run npm run preview
```

The SDK clears default run storage at startup. Copy results elsewhere or use a different directory for each run. The preview server binds only to this computer.

Set `SCREENSHOT_PREVIEW_PORT` to run a separate gallery alongside an existing one:

```sh
CRAWLEE_STORAGE_DIR=./storage/fleet-sites SCREENSHOT_PREVIEW_PORT=4322 npm run preview
```

The running local preview is accessible only on this computer. The gallery itself is also a production output: on Apify, each run saves `GALLERY.html` and its screenshot files in cloud storage. Generated public or signed record links can grant access to people holding those links, so cloud output should not be assumed private solely because it came from a private actor.

### Input

```json
{
  "urls": [{ "url": "https://example.com" }],
  "devices": ["desktop", "mobile"],
  "format": "png",
  "fullPage": true,
  "scrollToBottom": true,
  "cookieConsent": "reject",
  "dismissModals": true,
  "maxConcurrency": 2,
  "maxRetries": 1
}
```

`urls` accepts strings or objects containing only `url`. Use explicit HTTP(S) addresses. Repeated normalized URLs and devices are deduplicated; fragments remain distinct for applications with hash routing. The Actor does not follow links or expand URL-list files. Unknown input fields fail validation.

| Option | Default | Behavior |
| --- | --- | --- |
| `devices` | `["desktop"]` | Any combination of desktop, tablet, mobile |
| `format` | `png` | `png`, `jpeg`, or `pdf` |
| `quality` | `85` | JPEG quality, 1–100 |
| `fullPage` | `true` | Full page up to the image height limit; false captures the viewport |
| `viewportWidth` / `viewportHeight` | `1440` / `900` | Desktop viewport; tablet is 768 × 1024, mobile 390 × 664 |
| `maxScreenshotHeight` | `12000` | Image height cap; cropped images have `truncated: true` |
| `captureSelector` | empty | Capture the first visible matching element; unavailable for PDF |
| `waitUntil` | `load` | `load`, `domcontentloaded`, or `networkidle` |
| `waitForSelector` | empty | Require a visible element before capture |
| `delay` | `500` | Additional wait after navigation, in milliseconds |
| `navigationTimeoutSecs` | `30` | Navigation deadline per attempt |
| `captureTimeoutSecs` | `60` | Entire capture deadline per attempt |
| `assetTimeoutSecs` | `10` | Wait for images in the capture area to load and decode, allowing the image set and page height to stabilize |
| `failOnMissingImages` | `true` | Return `ASSET_LOAD_FAILED` without a screenshot if a required image remains unavailable; false saves a diagnostic capture with issues |
| `scrollToBottom` | `false` | Trigger lazy content, then return to the top |
| `maxScrollTimeSecs` | `10` | Scrolling deadline, also bounded by the image height cap |
| `cookieConsent` | `reject` | Prefer rejecting optional cookies; fall back to accept/close when needed. `accept` reverses the preference; `none` preserves consent overlays |
| `dismissModals` | `true` | Close other detected modals through an explicit close, dismiss, or no-thanks control |
| `hideCookieBanners` | `false` | Hide known banner elements; does not click consent |
| `selectorsToHide` | empty | CSS selectors, one per line; runtime also accepts a string array |
| `darkMode` | `false` | Request the browser dark color scheme |
| `blockMedia` | `true` | Block video/audio downloads while retaining images and fonts |
| `maxConcurrency` | `2` | Isolated contexts running in parallel, up to 8 |
| `maxRetries` | `1` | Extra attempts, up to 3 |
| `maxCaptures` | `100` | Reject input above this URL × device count; maximum 1000 |
| `proxy` | omitted | Optional Apify or custom proxy configuration |
| `allowPrivateUrls` | `false` | Local testing only; rejected on Apify |

The Actor uses full Chromium in its new headless mode. Desktop uses a browser-version-matched Chrome user agent; mobile and tablet use device user agents and touch emulation. All images use a device scale factor of 1, so dimensions describe CSS pixels. This is Chromium emulation, not a physical device or Safari test. Proxy connections share one session within each capture attempt; retries request a fresh session.

All duration fields ending in `Secs` use seconds. `delay` uses milliseconds. PDF uses the screen stylesheet and prints the complete document to A4 pages, respecting a site's CSS page size. PDF ignores the image height cap and JPEG quality; `fullPage` must be true. Its image width and height are null. Element screenshots fail if they exceed the image size limit.

Retries apply to navigation errors, capture deadlines, HTTP 429, HTTP 5xx, and transient image-loading failures, with short incremental backoff. When a proxy is configured, HTTP 403 also retries with a fresh proxy session. An HTTP/2 protocol error switches the next configured retry to a separate HTTP/1.1 browser and records a warning; this does not guarantee that a site will accept the connection. Other HTTP 4xx errors and permanent image decoding failures are final. A ready-selector failure is final. The default maximum capture time is approximately two 60-second attempts plus backoff, browser startup, and storage operations. The whole-run timeout is configured separately in Apify run options. Deadline errors retain known navigation and image metadata and identify the active capture stage.

Consent handling runs before scrolling and again before capture, including a brief check for delayed dialogs. Finite animations finish before these checks, so their completion handlers can expose dialogs before screenshot encoding. Hidden and offscreen iframes do not consume the visible-frame scan limit. The Actor uses actual controls inside detected consent dialogs and prefers rejection by default. It retries a control to allow for delayed page hydration, and can accept or close when the preferred control is unavailable or ineffective. Other detected modals use only explicit close/dismiss controls. An unresolved blocker produces `OVERLAY_BLOCKED` with its detected controls and no screenshot; configured retries use fresh contexts. A blocker that persists through all attempts remains a failure. Set `cookieConsent: "none"` and `dismissModals: false` when the original overlay state is intentional. `hideCookieBanners` and `selectorsToHide` remain explicit visual-hiding overrides, applied before automatic dismissal.

Working live-site inputs are included for [Prada](examples/prada.json) and [Gucci](examples/gucci-residential.json). The Gucci example uses a US residential proxy, one capture at a time, up to three retries, and `domcontentloaded` followed by explicit image readiness checks. Proxy use remains an input choice.

Image readiness uses the chosen capture area: full-page images up to the height cap, the current viewport, or the selected element. CSS-hidden and fully clipped images are excluded, as are images outside that area. Relevant native lazy images are loaded eagerly; common `data-src` and `data-srcset` placeholders are initialized. Scrolling uses instant steps so site-wide smooth scrolling cannot prevent distant sections from being reached. The Actor waits for decoding and a short stable image/layout window. An unresolved required image fails the capture by default, including a source URL and HTTP/decoding reason in `assets.issues`. Diagnostic mode preserves those issues and saves the image for investigation.

### Output

Each dataset item includes:

```json
{
  "id": "deterministic-url-and-device-id",
  "startUrl": "https://example.com/",
  "url": "https://example.com/",
  "device": "desktop",
  "format": "png",
  "ok": true,
  "httpStatus": 200,
  "title": "Example Domain",
  "screenshotKey": "capture-desktop-<id>.png",
  "screenshotUrl": "<signed-or-public-Apify-storage-URL>",
  "screenshotPath": null,
  "width": 1440,
  "height": 900,
  "pageHeight": 900,
  "truncated": false,
  "bytes": 12345,
  "sha256": "<content-hash>",
  "attempts": 1,
  "durationMs": 1400,
  "capturedAt": "<ISO-timestamp>",
  "error": null,
  "errorType": null,
  "warnings": [],
  "overlayActions": [],
  "assets": {
    "totalImages": 3,
    "relevantImages": 1,
    "loadedImages": 1,
    "pendingImages": 0,
    "failedImages": 0,
    "ignoredHiddenImages": 1,
    "ignoredOutsideCapture": 1,
    "fontsReady": true,
    "issues": []
  }
}
```

This example describes the fields; values are illustrative. Locally, `screenshotUrl` is null and `screenshotPath` points to the actual file. On Apify, `screenshotPath` is null and the SDK supplies the storage URL, including a signature when required. Preserve the full URL.

A captured HTTP error page has `ok: false`, `errorType: "HTTP_ERROR"`, its status code, and a diagnostic screenshot when the capture can otherwise complete. A navigation, selector, unresolved-overlay, or required-image failure has no screenshot by default. Warnings identify invalid hide selectors, bounded scrolling, cropping, or incomplete assets in diagnostic mode. `ok` reports HTTP and capture success; a bot challenge returned with HTTP 200 can still be marked successful.

`overlayActions` records each clicked control, for example `{ "kind": "cookie-consent", "action": "reject", "button": "Reject Non-Essential" }`. The gallery shows those actions alongside the capture. When an overlay cannot be dismissed, the error row retains known HTTP status and actions but has no binary artifact.

`assets` reports loaded/relevant image counts and excluded image counts. Each issue includes `url`, `alt`, `state`, `reason`, and available `httpStatus` and `contentType`. The gallery displays image readiness and issue reasons. `assets` is null when a capture fails before reaching the asset checks. Counts can change between runs as a page adds or removes images.

The key-value store contains:

- `capture-<device>-<id>.<extension>` — image or PDF.
- `RESULTS` — the complete result array, in URL/device order.
- `SUMMARY` — success/failure counts and elapsed time.
- `GALLERY.html` — visual gallery, including errors.

The dataset streams completed rows, so its order may differ from the input order. A partial-success batch finishes successfully. When every capture fails, the Actor saves its results and exits with an error. Storage or browser infrastructure failures also fail the run. An externally aborted run can leave a partial dataset without a final gallery.

### Tests

```sh
npm run check
npm test
npm run test:integration
npm run build
npm run smoke
```

Integration tests use Chromium and local fixtures. They inspect image pixels and dimensions, PDF signatures, redirects, HTTP failures/retries, lazy loading, cropping, selector behavior, capture deadlines, and infinite-scroll bounds. Overlay tests cover reject/accept preferences, accept-only consent, delayed and iframe dialogs, modal close controls, opt-out, and explicit failure when a blocker remains. They also verify that unrelated Accept All buttons are left alone. Asset tests cover slow decoding/loading, hidden and clipped images, viewport/element boundaries, deferred sources, smooth-scroll interference, real HTTP/decoding failures, bounded deadlines, diagnostic mode, and retries. The live smoke test additionally checks example.com and apify.com; it requires network access and can fail when either site changes or blocks automation.

For a deployed Actor, run the opt-in cloud suite against an explicit build:

```sh
npm run test:cloud -- --build 0.1.14 --report storage/cloud-tests/fresh-verification
```

The suite uses `APIFY_TOKEN` or the existing Apify CLI 1.10.0 keyring login. It starts bounded, billable cloud runs in the authenticated owner's account, downloads artifacts, and checks pixels, dimensions, hashes, result completeness, output links, validation, and expected failures. Some runs deliberately finish with `FAILED`; the test passes when the recorded behavior matches the case. Public fixture services and live sites must be reachable. `--list` prints the cases without using credentials or starting runs. Reusing the same report directory resumes saved run IDs and skips verified cases. Use a new directory for a fresh retest.

The named `famous-*`, `fixed-*`, `gucci-*`, and proxy cases require explicit selection with `--only`; they are excluded from the default suite. See the [timeout fix and cloud verification](docs/TIMEOUT-FIX.md) for the current release evidence and working site inputs. Previous results remain in the [earlier cloud audit](docs/FAMOUS-SITES-AUDIT.md).

### Deploy and publish

The [release guide](docs/RELEASE.md) covers GitHub, a private Apify deployment, a cloud smoke test, and Store publication. The Dockerfile installs the Chromium version associated with the lockfile. `npm ci` reproduces the dependency tree.

This repository does not deploy automatically. GitHub CI runs the local checks and browser tests. No token is needed for local development. Use GitHub/Apify's normal credential storage or environment variables when deploying, and keep tokens out of source files.

### Limits and tradeoffs

- A screenshot records what the browser rendered. Login walls, bot challenges, and undetected overlays can still appear. Detected blocking dialogs must be cleared and required images must be ready before a default capture succeeds. The Actor does not solve CAPTCHAs or sign in.
- Image readiness checks DOM image elements in the main document; CSS backgrounds, canvas/WebGL content, video frames, embedded frame content, and arbitrary application animation state are not certified by the image counts. Video/audio downloads remain blocked by default. Asset waits are bounded by `assetTimeoutSecs` and the total capture deadline. Diagnostic mode can intentionally save incomplete images with explicit issues.
- Overlay detection uses dialog semantics, known consent containers, and English control labels. Custom implementations, other languages, closed shadow roots, or dialogs appearing after the last check can require site-specific handling. Explicit CSS hiding can alter layout and does not record consent.
- The height limit controls image output and scrolling, while PDF prints the whole document. Large PDFs and heavy pages still need memory and a sensible whole-run deadline.
- Normal captures share one Chromium process; an HTTP/1.1 fallback process starts only after an HTTP/2 protocol error. Each attempt has its own clean context, and both browsers close at run completion. There is no persistent queue, automatic migration resume, browser-crash recovery, visual diffing, or baseline history in this first version.
- Public-address checks block private/reserved targets and subresources as a best effort. DNS rebinding and proxy-side DNS resolution prevent this from being a network security boundary. Deploy only in an environment with suitable network isolation.
- SHA-256 hashes identify byte-identical files. Dynamic content and metadata can change hashes; a hash difference is not a visual change score.
- Apify compute, storage, transfer, and optional proxy usage follow the live Pricing tab's platform-cost setting. Retention and later storage access follow the account's storage policy.

Source code is independently written. The reference actor's branding and documentation are not used as this product's own.

# Actor input Schema

## `urls` (type: `array`):

Explicit HTTP(S) URLs, as strings or objects with a url property. URL-list files and crawling are not supported.

## `devices` (type: `array`):

Each selected device produces a separate capture. All images use one image pixel per CSS pixel.

## `format` (type: `string`):

PNG for lossless images, JPEG for smaller files, PDF for the complete document on A4 pages.

## `quality` (type: `integer`):

JPEG quality, from 1 to 100. Ignored for PNG and PDF.

## `fullPage` (type: `boolean`):

Capture the full page up to the height limit. Disable for the viewport only. PDF requires true.

## `viewportWidth` (type: `integer`):

Desktop width in CSS pixels. Mobile and tablet use their device presets.

## `viewportHeight` (type: `integer`):

Desktop height in CSS pixels. Mobile and tablet use their device presets.

## `maxScreenshotHeight` (type: `integer`):

Maximum image height and scrolling distance. Must be at least 1024 and the desktop viewport height. PDF prints the complete document; this limit only bounds its scrolling.

## `captureSelector` (type: `string`):

Optional CSS selector for one element. Oversized elements fail explicitly. Not available for PDF.

## `waitUntil` (type: `string`):

Navigation readiness event. Network idle can time out on pages with continuous traffic.

## `waitForSelector` (type: `string`):

Optional CSS selector that must become visible before capture. Failure produces an error row.

## `delay` (type: `integer`):

Extra wait after navigation, in milliseconds.

## `navigationTimeoutSecs` (type: `integer`):

Maximum seconds for a navigation attempt.

## `captureTimeoutSecs` (type: `integer`):

Maximum seconds for an entire capture attempt, including navigation, rendering, and encoding.

## `assetTimeoutSecs` (type: `integer`):

Maximum seconds waiting for images in the capture area to load and decode. Hidden and out-of-scope images are excluded.

## `failOnMissingImages` (type: `boolean`):

Fail without saving a screenshot when an image needed by the capture remains unavailable. Disable to save a diagnostic screenshot with explicit asset issues.

## `scrollToBottom` (type: `boolean`):

Scroll to trigger lazy content, bounded by the time and image height limits, then return to the top.

## `maxScrollTimeSecs` (type: `integer`):

Maximum seconds spent scrolling in each attempt.

## `cookieConsent` (type: `string`):

Automatically dismiss consent dialogs. Prefer rejecting optional cookies by default, with accept/close fallback. Use none to preserve consent overlays.

## `dismissModals` (type: `boolean`):

Close other detected modal dialogs using an explicit close, dismiss, or no-thanks control. Fail the capture if a detected blocking dialog remains.

## `hideCookieBanners` (type: `boolean`):

Hide banners matched by a small list of known selectors. Does not click consent or guarantee removal.

## `selectorsToHide` (type: `string`):

CSS selectors to hide before capture, one selector per line. A comma-separated CSS selector is also valid.

## `darkMode` (type: `boolean`):

Use the browser dark color scheme. Only pages that support it will change appearance.

## `blockMedia` (type: `boolean`):

Block video and audio downloads. Images, styles, scripts, and fonts remain enabled.

## `maxConcurrency` (type: `integer`):

Number of isolated browser contexts running concurrently. Start with 2 on a 2 GB Actor.

## `maxRetries` (type: `integer`):

Extra attempts for navigation failures, deadlines, unresolved dialogs, transient image failures, HTTP 429, and HTTP 5xx. Proxied HTTP 403 responses also retry. Other HTTP errors are final.

## `maxCaptures` (type: `integer`):

Reject input when the number of unique URL × device captures exceeds this limit.

## `allowPrivateUrls` (type: `boolean`):

Local testing only. Must remain false on Apify.

## `proxy` (type: `object`):

Optional Apify Proxy or custom proxy configuration. Omitting it uses a direct connection.

## Actor input object example

```json
{
  "urls": [
    {
      "url": "https://example.com"
    }
  ],
  "devices": [
    "desktop"
  ],
  "format": "png",
  "quality": 85,
  "fullPage": true,
  "viewportWidth": 1440,
  "viewportHeight": 900,
  "maxScreenshotHeight": 12000,
  "captureSelector": "",
  "waitUntil": "load",
  "waitForSelector": "",
  "delay": 500,
  "navigationTimeoutSecs": 30,
  "captureTimeoutSecs": 60,
  "assetTimeoutSecs": 10,
  "failOnMissingImages": true,
  "scrollToBottom": false,
  "maxScrollTimeSecs": 10,
  "cookieConsent": "reject",
  "dismissModals": true,
  "hideCookieBanners": false,
  "selectorsToHide": "",
  "darkMode": false,
  "blockMedia": true,
  "maxConcurrency": 2,
  "maxRetries": 1,
  "maxCaptures": 100,
  "allowPrivateUrls": false
}
```

# Actor output Schema

## `captures` (type: `string`):

No description

## `gallery` (type: `string`):

No description

## `summary` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "urls": [
        {
            "url": "https://example.com"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("aster_data/website-screenshot-studio").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "urls": [{ "url": "https://example.com" }] }

# Run the Actor and wait for it to finish
run = client.actor("aster_data/website-screenshot-studio").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "urls": [
    {
      "url": "https://example.com"
    }
  ]
}' |
apify call aster_data/website-screenshot-studio --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,aster_data/website-screenshot-studio"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/ZNb1DvfvhnucO3l7n/builds/QetF7VsP6FbNFsZEK/openapi.json
