# MP4 Renderer for HyperFrames (Unofficial) (`optimdex/hyperframes-render`) Actor

Render a HyperFrames HTML video composition (ZIP in a key-value store, or inline HTML) to an MP4, up to 1080p and 120 s per run. Unofficial; not affiliated with HeyGen.

- **URL**: https://apify.com/optimdex/hyperframes-render.md
- **Developed by:** [Salman AlFariz](https://apify.com/optimdex) (community)
- **Categories:** Videos, Developer tools
- **Stats:** 1 total users, 1 monthly users, 89.5% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $100.00 / 1,000 render completeds

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

## MP4 Renderer for HyperFrames (Unofficial)

Unofficial. Not affiliated with, endorsed by, or sponsored by HeyGen. HyperFrames is a project of HeyGen, Inc.

Turn a [HyperFrames](https://github.com/heygen-com/hyperframes) HTML video composition into a ready-to-share MP4 in the cloud. Send a ZIP of your project or paste a single HTML file, and get back an H.264 MP4 up to 1080p and 120 seconds long. There's nothing to install: no Chrome, no ffmpeg, no render server to keep running. You pay only for videos that render successfully.

**Built for:** developers automating video, AI agents that write HTML compositions, marketing and product teams producing personalised clips from one template, and anyone who wants HyperFrames output without running their own render machine.

[![Tutorial: render a HyperFrames composition to MP4 with one API call. This video was rendered by this actor.](https://raw.githubusercontent.com/salmanalfariz24/salmanalfariz24/main/assets/hyperframes-render/hyperframes-tutorial-20fps.gif)](https://raw.githubusercontent.com/salmanalfariz24/salmanalfariz24/main/assets/hyperframes-render/hyperframes-tutorial-60fps.mp4)

*This 36-second tutorial was rendered by this actor (1080p, 60 fps). [Watch the full-quality MP4](https://raw.githubusercontent.com/salmanalfariz24/salmanalfariz24/main/assets/hyperframes-render/hyperframes-tutorial-60fps.mp4) or [see the source composition](https://github.com/salmanalfariz24/salmanalfariz24/tree/main/examples/hyperframes-tutorial).*

### What it does

One HyperFrames composition goes in, one MP4 comes out, per run.

| Feature | What you get |
| --- | --- |
| Two ways to send a composition | A project ZIP stored in one of your Apify key-value stores, or a single HTML file pasted inline |
| Up to Full HD | Any size up to 1920×1080, landscape or portrait (1080×1920 works too) |
| Up to 120 seconds | At 24, 30 or 60 frames per second |
| Three quality presets | `draft` (fastest), `standard` (default) or `high` (slowest, largest file), all at the same price |
| Variables | Pass a JSON object to your composition to render many versions of one template |
| Same price at any resolution | 320×180 and 1920×1080 cost the same; you pay for frames, not pixels |
| Price check before rendering | The actor works out the charge first and stops, uncharged, if it's over your maximum cost per run |
| No charge for failures | Failed, timed-out, too-expensive, stalled or aborted runs are never charged for rendering |
| Clear results | The MP4, a dataset row with duration, size, frames and charge, and an `OUTPUT` status record with an error code if something went wrong |
| Safe by default | ZIPs are checked for unsafe paths and oversized content, and the composition can't reach private networks |

### How to render your first video

#### Step 1: Open the actor and keep the example

The **Inline HTML** field comes prefilled with a 1-second, 320×180 "Hello" composition. Leave it as it is for your first run.

#### Step 2: Start the run

Click **Start**. The example renders in about 20 seconds and costs about $0.106 (see Pricing).

#### Step 3: Download your MP4

Open the run's **Output** tab and click the **MP4 video** link, or open **Storage → Key-value store** and download `render.mp4`. The dataset row has the same link in `videoUrl`.

#### Step 4: Render your own composition

- **Single HTML file with no local assets?** Paste it into **Inline HTML**.
- **Project with images, fonts, video or sub-compositions?** Zip it, upload the ZIP to a key-value store (see "Prepare a ZIP project" below), then pick that store and enter the record key. When a ZIP is set, the prefilled HTML is ignored, so you don't need to clear it.

Before longer renders, raise **Maximum cost per run** in the run options so the price check passes. A 120-second video at 30 fps needs at least $0.71.

### Pricing

This actor uses pay-per-event pricing. You pay a fixed price per successful render and per second of video, and nothing for Apify platform usage.

| Event | Price | When it's charged |
| --- | --- | --- |
| `render-completed` | $0.10 | Once, when a render succeeds |
| `frames-30` | $0.005 | Per 30 output frames, rounded up, when a render succeeds |
| `apify-actor-start` | $0.00005 per GB of run memory | When any run starts: $0.0008 at the default 16 GB, $0.0004 at 8 GB |

30 frames is one second of 30 fps video, so each second at 30 fps costs $0.005, and each second at 60 fps costs $0.01. The number of `frames-30` events is the `frameUnits` field in the output: total frames divided by 30, rounded up. Resolution and quality don't change the price.

#### Cost examples

| Video | Frames | `frames-30` events | Render cost | Total with start fee (16 GB) |
| --- | --- | --- | --- | --- |
| 1 s at 320×180, 30 fps (the prefilled example) | 30 | 1 | $0.105 | $0.1058 |
| 10 s at 30 fps | 300 | 10 | $0.15 | $0.1508 |
| 15 s vertical 1080×1920 at 24 fps | 360 | 12 | $0.16 | $0.1608 |
| 30 s at 1080p, 30 fps | 900 | 30 | $0.25 | $0.2508 |
| 30 s at 1080p, 60 fps | 1,800 | 60 | $0.40 | $0.4008 |
| 60 s at 30 fps | 1,800 | 60 | $0.40 | $0.4008 |
| 120 s at 1080p, 30 fps | 3,600 | 120 | $0.70 | $0.7008 |
| 120 s at 1080p, 60 fps (the most a run can cost) | 7,200 | 240 | $1.30 | $1.3008 |

#### What you're never charged for

- **Failed runs.** Every error code below ends the run before anything is charged, apart from the start fee.
- **Timed-out, stalled, too-expensive or aborted runs.** Same rule: only the start fee.

Renders that **complete with warnings** (for example a missing font or a blocked asset) are delivered and charged, and each warning is listed in the output.

#### Maximum cost per run

Before rendering, the actor checks that the expected charge fits your maximum cost per run. If it doesn't, the run stops with `EXPECTED_CHARGE_EXCEEDS_MAX` and nothing is charged.

- The smallest allowed maximum is **$0.11**, which covers a 1-second render.
- Set it higher for longer videos, using the cost examples above. In the API it's the `maxTotalChargeUsd` parameter.
- Add `data-duration` to your composition's root element so the check happens before any browser starts.

Prices shown are launch prices. **The Store Pricing tab is authoritative.**

### Input

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `projectZipStore` | string | none | The key-value store that holds your project ZIP. In Console, pick it with the store picker; in the API, send the store ID or name. The actor gets read access to this store only. |
| `projectZipKey` | string | none | The record key of the ZIP in that store, for example `project.zip`. Required together with `projectZipStore`. |
| `html` | string | prefilled 1-second example in Console | A complete single-file composition, up to 1,000,000 bytes. Ignored when `projectZipStore` and `projectZipKey` are set. |
| `entryFile` | string | `index.html` | Path of the entry HTML file inside the ZIP, for example `scenes/main.html`. ZIP only. |
| `format` | string | `mp4` | Output format. Only `mp4` (H.264) is available. |
| `fps` | integer | `30` | Frames per second: `24`, `30` or `60`. |
| `quality` | string | `standard` | Encoder preset: `draft`, `standard` or `high`. |
| `variables` | object | `{}` | JSON object passed to your composition as HyperFrames variables, up to 64 KB. |

Give exactly one source: a ZIP (store plus key) or inline HTML. If you give both, the ZIP is used, the HTML is ignored, and the run adds this warning: "Both a project ZIP and inline HTML were given; the ZIP was used and the HTML was ignored."

#### What your composition needs

- A root element with `data-composition-id`.
- `data-width` and `data-height` on the root, as even numbers. They set the MP4's size: at least 16 px, long edge up to 1920, short edge up to 1080.
- `data-duration` in seconds on the root, up to 120. It's optional, but it lets the price check run before any browser starts.
- Only relative paths to files inside the project. Public web assets (fonts, images, video) over the internet are fine.

#### Example: inline HTML (1 s, 320×180)

This is the prefilled example. It fits the smallest maximum cost per run.

```html
<!DOCTYPE html>
<html><body>
<div data-composition-id="demo" data-width="320" data-height="180" data-duration="1" data-no-timeline data-fps="30"
     style="width:320px;height:180px;background:#222;color:#fff;display:flex;align-items:center;justify-content:center;">
  Hello
</div>
</body></html>
```

As actor input:

```json
{
  "html": "<!DOCTYPE html>\n<html><body>\n<div data-composition-id=\"demo\" data-width=\"320\" data-height=\"180\" data-duration=\"1\" data-no-timeline data-fps=\"30\"\n     style=\"width:320px;height:180px;background:#222;color:#fff;display:flex;align-items:center;justify-content:center;\">\n  Hello\n</div>\n</body></html>",
  "fps": 30,
  "quality": "standard"
}
```

#### Example: project ZIP

```json
{
  "projectZipStore": "my-hyperframes-projects",
  "projectZipKey": "project.zip",
  "entryFile": "index.html",
  "fps": 30,
  "quality": "standard"
}
```

#### Example: 60 fps, high quality, with variables

```json
{
  "projectZipStore": "my-hyperframes-projects",
  "projectZipKey": "promo.zip",
  "fps": 60,
  "quality": "high",
  "variables": { "headline": "Spring sale", "price": "$29" }
}
```

#### Prepare a ZIP project

1. Put your entry file at the root of the ZIP as `index.html`, or set `entryFile` to its path. To try it, save the inline example above as `index.html` and zip it.
2. Keep every local asset inside the ZIP and reference it with a relative path.
3. Upload the ZIP to a key-value store:
   - **In Console:** open **Storage → Key-value stores**, create or open a store, and upload the ZIP as a record.
   - **With the API:** replace `<storeId>` with your store's ID.

```bash
curl -X PUT \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/zip" \
  --data-binary @project.zip \
  https://api.apify.com/v2/key-value-stores/<storeId>/records/project.zip
```

ZIP limits: 100 MiB compressed, 500 MiB extracted and 2,000 files. Playlist files (`.m3u8`, `.m3u`, `.mpd`, `.ffconcat`, `.concat`) and symlinks aren't allowed.

### Limits

| Limit | Value |
| --- | --- |
| Output | MP4 (H.264) only, one video per run |
| Size | Up to 1920×1080, landscape or portrait; at least 16 px per side; even numbers |
| Duration | Up to 120 s |
| Frame rate | 24, 30 or 60 fps |
| Inline HTML | 1,000,000 bytes |
| Variables | 64 KB |
| ZIP | 100 MiB compressed, 500 MiB extracted, 2,000 files, no playlist files or symlinks, relative paths only |
| External downloads | 200 MiB per run; private and internal network addresses are blocked |
| Memory | 8 GB to 16 GB; 16 GB is the default and recommended |

**Cost budget.** Each run may use platform resources (compute and data transfer) worth at most what its price covers. Compositions that would take longer, or download more than the price covers, are stopped early with `RENDER_COST_LIMIT` and not charged. Reduce fps, quality, duration or external downloads, or keep the default 16 GB memory.

### Output

Every run writes to its default storages:

| Where | Name | When | What it is |
| --- | --- | --- | --- |
| Key-value store | `render.mp4` | Success only | The video (H.264, yuv420p) |
| Key-value store | `OUTPUT` | Always (success, failure or abort) | Status record: succeeded, failed or aborted, whether the run was charged, and the error code on failure |
| Dataset | one item | Success only | Duration, size, frames, expected charge, timings, warnings and the MP4 link |

The run's status message tells you the result at a glance, for example `Rendered 320x180, 1 s, …` or `Failed [EXPECTED_CHARGE_EXCEEDS_MAX]: …`.

#### Dataset item (from a real 1-second run, signature masked)

```json
{
  "status": "succeeded",
  "videoUrl": "https://api.apify.com/v2/key-value-stores/dGY54mBAoHpV9H3tl/records/render.mp4?signature=<signature>",
  "videoKey": "render.mp4",
  "format": "mp4",
  "durationSeconds": 1,
  "width": 320,
  "height": 180,
  "fps": 30,
  "quality": "standard",
  "totalFrames": 30,
  "frameUnits": 1,
  "expectedChargeUsd": 0.10500000000000001,
  "sizeBytes": 3079,
  "renderTimeMs": 6928,
  "totalRunTimeMs": 18764,
  "memoryMbytes": 16384,
  "workers": 3,
  "estimatedComputeUnits": 0.08339555555555556,
  "budgetComputeUnits": 0.4037625000000001,
  "externalBytes": 0,
  "blockedRequests": 0,
  "warnings": [],
  "producerVersion": "0.8.104",
  "chromeVersion": "148.0.7778.167"
}
```

`OUTPUT` on success has the same fields plus `runId`, `charged` (true or false) and `chargedEvents`, for example `{ "render-completed": 1, "frames-30": 1 }`.

#### `OUTPUT` when a run fails

```json
{
  "schemaVersion": 2,
  "status": "failed",
  "runId": "<runId>",
  "charged": false,
  "error": {
    "code": "EXPECTED_CHARGE_EXCEEDS_MAX",
    "class": "user",
    "phase": "precheck",
    "message": "Expected charge $0.70 (render-completed $0.10 + 120 x frames-30 $0.005) exceeds the remaining maximum cost per run ($0.50). Raise the maximum cost per run, or reduce duration or fps."
  },
  "frames": { "fps": 30, "quality": "standard", "totalFrames": 3600, "frameUnits": 120, "expectedChargeUsd": 0.70 },
  "producerVersion": "0.8.104",
  "chromeVersion": "148.0.7778.167"
}
```

Look up `error.code` in the error table below for what it means and how to fix it.

#### Downloading the MP4

```bash
curl -L -o render.mp4 \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  "https://api.apify.com/v2/key-value-stores/<defaultKeyValueStoreId>/records/render.mp4"
```

**Signed links.** `videoUrl` is normally a signed link. Signed links **do not expire**, so anyone who has the link can download the MP4. Share it as you would share the file.

### Data retention

Each run's default storages are unnamed. Unnamed storages follow your plan's data retention and **may be deleted after 7 days**; on the Free plan the 10 most recent runs are kept for 4 months. Download the MP4, or copy it to a named key-value store, if you need to keep it.

### Use cases

- **Personalised videos.** Build one composition, then render a version per customer, product or event by changing `variables`.
- **Automated social clips.** Generate vertical 1080×1920 or square videos from templates on a schedule.
- **AI agent pipelines.** Let an agent write HyperFrames HTML and call this actor to turn it into a video file.
- **No-code workflows.** Start renders from Make, Zapier or n8n through Apify integrations and webhooks, and pick up the MP4 link when the run finishes.
- **Data and product videos.** Turn reports, release notes or listings into short explainer videos without opening a video editor.

### Use it from your code

Replace `APIFY_TOKEN` with your token from **Settings → API & Integrations** in Apify Console. The Store's **API** tab has more clients and an OpenAPI definition.

#### Node.js

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });

const run = await client.actor('optimdex/hyperframes-render').call({
    projectZipStore: 'my-hyperframes-projects',
    projectZipKey: 'project.zip',
    fps: 30,
    quality: 'standard',
});

// Success: one dataset item with the MP4 link
const { items } = await client.dataset(run.defaultDatasetId).listItems();
if (items.length > 0) {
    console.log('MP4:', items[0].videoUrl);
} else {
    // Failure or abort: OUTPUT holds the error code
    const output = await client.keyValueStore(run.defaultKeyValueStoreId).getRecord('OUTPUT');
    console.log('Failed:', output?.value);
}
```

#### cURL (short renders)

This waits for the run and returns the dataset item. Apify's synchronous endpoints wait up to 5 minutes, so for long renders start a normal run and read the results when it finishes.

```bash
curl -X POST \
  "https://api.apify.com/v2/acts/optimdex~hyperframes-render/run-sync-get-dataset-items?maxTotalChargeUsd=0.11" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "html": "<!DOCTYPE html><html><body><div data-composition-id=\"demo\" data-width=\"320\" data-height=\"180\" data-duration=\"1\" data-no-timeline data-fps=\"30\" style=\"width:320px;height:180px;background:#222;color:#fff;display:flex;align-items:center;justify-content:center;\">Hello</div></body></html>"
  }'
```

### Error codes

A failed run never charges for rendering; you pay only the start fee. The code appears in the run's status message as `Failed [CODE]: …` and in `OUTPUT.error.code`. **User** errors are fixable in your input or composition (exit code 2). **System** errors are on our side (exit code 3); if one repeats, report it in the Issues tab with the run ID.

#### Input

| Code | What it means | How to fix it |
| --- | --- | --- |
| `INPUT_INVALID` | A field has the wrong type or value. Common causes: empty `html`, a ZIP store without a key (or a key without a store), a store or key that looks like a URL, a bad `entryFile`, an unknown `quality`, `variables` that isn't a JSON object, or `entryFile` other than `index.html` with inline HTML. | Read the message for the field name. Give the store and key together, use only the record key (not a URL), and leave `entryFile` at `index.html` when using inline HTML. |
| `INPUT_UNKNOWN_FIELD` | The input has a field this actor doesn't accept, such as `width`, `height` or `archiveUrl`. | Remove the listed fields. Size comes from the composition's `data-width` and `data-height`. |
| `INPUT_SOURCE_MISSING` | Neither a ZIP (store plus key) nor `html` was given. | Add one source. |
| `FORMAT_UNSUPPORTED` | `format` isn't `mp4`. | Use `mp4`. |
| `FPS_UNSUPPORTED` | `fps` isn't 24, 30 or 60. | Use 24, 30 or 60. |
| `HTML_TOO_LARGE` | `html` is over 1,000,000 bytes. | Move assets out of the HTML and send a ZIP instead. |
| `VARIABLES_TOO_LARGE` | `variables` is over 64 KB as JSON. | Send less data, or put large data in a file inside the ZIP. |

#### ZIP project

| Code | What it means | How to fix it |
| --- | --- | --- |
| `ZIP_RECORD_NOT_FOUND` | The store has no record with that key. | Check the key's spelling and case, and that you picked the right store. |
| `ZIP_ACCESS_DENIED` | The actor isn't allowed to read that store. | In Console, pick the store with the store picker so the actor gets read access. Through the API, use a store your token's account owns. |
| `ZIP_TOO_LARGE` | The ZIP is over 100 MiB. | Compress or shrink assets, or load large media from a public URL instead. |
| `ZIP_INVALID` | The file isn't a valid ZIP, or it's corrupt, encrypted or uses an unsupported compression method. | Re-create it with a standard zip tool, without a password. |
| `ZIP_UNSAFE_PATH` | An entry has an absolute path, `..`, a backslash, or a path that's too long, too deep or duplicated. | Zip from inside the project folder so every path is relative, and use forward slashes. |
| `ZIP_SYMLINK` | The ZIP contains a symlink or another special file. | Replace symlinks with real files. |
| `ZIP_TOO_MANY_FILES` | The ZIP has more than 2,000 entries. | Remove unused files or combine assets. |
| `ZIP_EXTRACTED_TOO_LARGE` | The contents are over 500 MiB once extracted, or the compression ratio looks like a ZIP bomb. | Reduce asset sizes. |
| `ZIP_UNSUPPORTED_FILE` | The ZIP contains a playlist file (`.m3u8`, `.m3u`, `.mpd`, `.ffconcat` or `.concat`). | Remove playlist files and use regular video files instead. |
| `ENTRY_FILE_NOT_FOUND` | The entry HTML file isn't in the ZIP. | Put `index.html` at the ZIP root, or set `entryFile` to the right path. |

#### Composition

| Code | What it means | How to fix it |
| --- | --- | --- |
| `COMPOSITION_INVALID` | The root element is missing `data-composition-id`, `data-width` or `data-height` is missing, odd or invalid, or the duration isn't positive. | Add `data-composition-id`, use even width and height, and give a positive `data-duration`. |
| `COMPOSITION_UNSAFE_PATH` | A `src`, `href`, `poster`, `data-composition-src` or `data-composition-file` uses an absolute path or `file:` link, or points outside the project, or a sub-composition isn't an `.html` file. Sub-compositions are checked too. | Use relative paths that stay inside the project, and point sub-compositions at `.html` files. |
| `RESOLUTION_LIMIT_EXCEEDED` | The long edge is over 1920 or the short edge is over 1080. | Use 1920×1080, 1080×1920 or smaller. |
| `DURATION_LIMIT_EXCEEDED` | The composition is longer than 120 s. | Shorten it, or split it across several runs. |

#### Cost and time limits

| Code | What it means | How to fix it |
| --- | --- | --- |
| `EXPECTED_CHARGE_EXCEEDS_MAX` | The render would cost more than your maximum cost per run. | Raise the maximum cost per run (`maxTotalChargeUsd` in the API) using the cost examples, or reduce duration or fps. |
| `EXTERNAL_DOWNLOAD_LIMIT` | The composition downloaded 200 MiB from the internet. | Put large media in the ZIP, or use smaller files. |
| `RENDER_COST_LIMIT` | The render was too slow, or downloaded too much, for its price. | Lower fps, quality, duration or external downloads, simplify heavy effects, or keep the default 16 GB memory. |
| `RENDER_TIMEOUT` | A render step or the run hit its time limit. | Raise the run timeout, simplify the composition, or shorten it. |
| `RENDER_STALLED` | No new frame for 60 s, or no first frame within 180 s. Often an endless loop in the composition's script, or an asset that never loads. | Check the composition renders in a normal browser, and remove scripts that wait forever. |
| `RUN_TIMEOUT_TOO_SHORT` | The run timeout leaves less than 2 minutes to render. | Raise the run timeout. 2,400 s covers every render this actor allows. |
| `OUTPUT_TOO_LARGE` | The MP4 came out larger than the size limit: 150 KB per frame (about 4.5 MB per second at 30 fps), and never more than 512 MiB. | Use `standard` or `draft` quality, or reduce noisy, high-detail motion. |

#### System

| Code | What it means | How to fix it |
| --- | --- | --- |
| `RENDER_FAILED` | The renderer or browser failed. | Try again. If it repeats, report it with the run ID. |
| `OUTPUT_VERIFY_FAILED` | The finished MP4 didn't pass the final check (wrong size, frame rate or duration). | Try again. If it repeats, report it with the run ID. |
| `STORAGE_WRITE_FAILED` | The MP4 couldn't be saved to storage. | Try again. |
| `RESTART_LIMIT_EXCEEDED` | The platform moved the run to a new server more than 3 times. | Try again. |
| `INTERNAL_ERROR` | Something unexpected went wrong. | Try again. If it repeats, report it with the run ID. |

### FAQ

#### What is HyperFrames?

[HyperFrames](https://github.com/heygen-com/hyperframes) is an open-source framework from HeyGen for writing videos as HTML, licensed under Apache-2.0. This actor runs its renderer for you in the cloud. It's an independent, unofficial service.

#### Do I need a HeyGen account?

No. You need only an Apify account.

#### Does resolution or quality change the price?

No. Price depends only on the number of frames: duration times fps. A 30-second video costs $0.25 at 30 fps whether it's 320×180 or 1920×1080, and whether quality is `draft` or `high`.

#### Am I charged when a run fails?

Only Apify's start fee ($0.0008 at 16 GB). Rendering is charged only when the MP4 is saved. Renders that finish with warnings are charged, and the warnings are listed in the output.

#### Can I render several videos in one run?

No, it's one video per run. Start several runs in parallel, each with its own input or `variables`.

#### How long does a render take?

The 1-second example finishes in about 20 seconds. Longer and heavier compositions take longer, and each run must finish within what its price covers (see Cost budget under Limits).

#### Can my composition load fonts, images or video from the web?

Yes, from public internet addresses, up to 200 MiB per run. Private and internal network addresses are blocked, and blocked requests are counted in `blockedRequests`. If an asset fails to load, the render usually still completes and a warning is added.

#### I gave both a ZIP and inline HTML. Which one is used?

The ZIP. The HTML is ignored and a warning is added to the output.

#### How do I keep my videos?

Download `render.mp4`, or copy it to a named key-value store. Unnamed run storages may be deleted after 7 days, depending on your plan.

#### Is the video link private?

`videoUrl` is normally a signed link that never expires, so anyone with the link can download the MP4. Share it as you would share the file itself.

#### Is this an official HeyGen product?

No. Unofficial. Not affiliated with, endorsed by, or sponsored by HeyGen. HyperFrames is a project of HeyGen, Inc.

### License and attribution

This actor is proprietary; its source code isn't public. It bundles third-party software under each project's own licence:

- **HyperFrames** (the `@hyperframes/*` packages), Copyright 2026 HeyGen, Inc., used unmodified under the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). Source: [heygen-com/hyperframes](https://github.com/heygen-com/hyperframes).
- **Chrome for Testing** (`chrome-headless-shell`), distributed by Google under its own terms: [Chrome for Testing](https://github.com/GoogleChromeLabs/chrome-for-testing).
- **ffmpeg**, from Debian: [ffmpeg licence](https://ffmpeg.org/legal.html) and [Debian package details](https://packages.debian.org/bookworm/ffmpeg).

# Changelog

This Actor's version history is a separate document: https://apify.com/optimdex/hyperframes-render/changelog.md

# Actor input Schema

## `projectZipStore` (type: `string`):

The key-value store that holds your project ZIP as a record. Upload the .zip in Console (Storage > Key-value stores > your store > upload a record) or with the API. Max 100 MiB compressed, 500 MiB extracted, 2,000 files. The actor gets read access to this store only.

## `projectZipKey` (type: `string`):

Key of the ZIP record in the store above, for example project.zip. Required together with the store.

## `html` (type: `string`):

A complete single-file composition (max 1,000,000 bytes). Prefilled with a 1-second example; ignored when project ZIP store and key are set.

## `entryFile` (type: `string`):

Path of the HTML entry file inside the ZIP.

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

v1 renders MP4 (H.264, yuv420p) only.

## `fps` (type: `integer`):

24, 30 or 60. Price scales with output frames: 60 fps costs twice as much per second as 30 fps.

## `quality` (type: `string`):

Encoder preset. draft is fastest; high is slowest and largest.

## `variables` (type: `object`):

Optional JSON object passed to the composition as HyperFrames variables (max 64 KB serialized).

## Actor input object example

```json
{
  "html": "<!DOCTYPE html>\n<html><body>\n<div data-composition-id=\"demo\" data-width=\"320\" data-height=\"180\" data-duration=\"1\" data-no-timeline data-fps=\"30\"\n     style=\"width:320px;height:180px;background:#222;color:#fff;display:flex;align-items:center;justify-content:center;\">\n  Hello\n</div>\n</body></html>",
  "entryFile": "index.html",
  "format": "mp4",
  "fps": 30,
  "quality": "standard",
  "variables": {}
}
```

# Actor output Schema

## `video` (type: `string`):

The rendered video (key render.mp4 in the default key-value store). Missing if the run failed or was aborted.

## `result` (type: `string`):

One dataset item with duration, resolution, frames, expected charge, size and timings.

## `status` (type: `string`):

OUTPUT record: succeeded/failed/aborted, whether the run was charged, and the error code on failure.

# 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 = {
    "html": `<!DOCTYPE html>
<html><body>
<div data-composition-id="demo" data-width="320" data-height="180" data-duration="1" data-no-timeline data-fps="30"
     style="width:320px;height:180px;background:#222;color:#fff;display:flex;align-items:center;justify-content:center;">
  Hello
</div>
</body></html>`,
    "variables": {}
};

// Run the Actor and wait for it to finish
const run = await client.actor("optimdex/hyperframes-render").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 = {
    "html": """<!DOCTYPE html>
<html><body>
<div data-composition-id=\"demo\" data-width=\"320\" data-height=\"180\" data-duration=\"1\" data-no-timeline data-fps=\"30\"
     style=\"width:320px;height:180px;background:#222;color:#fff;display:flex;align-items:center;justify-content:center;\">
  Hello
</div>
</body></html>""",
    "variables": {},
}

# Run the Actor and wait for it to finish
run = client.actor("optimdex/hyperframes-render").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 '{
  "html": "<!DOCTYPE html>\\n<html><body>\\n<div data-composition-id=\\"demo\\" data-width=\\"320\\" data-height=\\"180\\" data-duration=\\"1\\" data-no-timeline data-fps=\\"30\\"\\n     style=\\"width:320px;height:180px;background:#222;color:#fff;display:flex;align-items:center;justify-content:center;\\">\\n  Hello\\n</div>\\n</body></html>",
  "variables": {}
}' |
apify call optimdex/hyperframes-render --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,optimdex/hyperframes-render"
        }
    }
}
```

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/C3TMThRQcp2QJdo9D/builds/c7AX64eymPau98OKp/openapi.json
