> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://fastpix.com/docs/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://fastpix.com/docs/_mcp/server.

# Add video upload and playback to Astro

Uploading video is different from uploading other files. Large uploads need to resume after interruptions, and media must be processed into streaming formats before they can be played reliably across devices.

FastPix handles uploads, processing, and adaptive streaming. Your application only needs to create a signed upload URL on the server and render a player. Uploads go directly from the browser to FastPix, so video data never passes through your Astro server.

This guide uses the FastPix Astro Uploader, which is an Astro native component, so no React or other UI framework is required. If you'd rather work at a lower level, [the quickstart](/docs/get-started/quickstart) covers the same workflow using the API directly, and [resumable uploads for web](/docs/upload-videos/set-up-resumable-uploads-for-web) covers the underlying `@fastpix/resumable-uploads` engine.

\


## How it works

The upload workflow is straightforward:

1. Your Astro server creates a signed upload URL using the FastPix Node SDK.
2. The Astro Uploader uploads the selected file directly to FastPix using the signed URL.
3. FastPix processes the uploaded media asynchronously.
4. FastPix sends a `video.media.ready` webhook when the media is ready for playback.
5. Your application retrieves the playback ID and plays the video using the FastPix Player.

\


## What you'll build

By the end of this guide, you'll have:

* An Astro application that uploads videos directly to FastPix.
* Resumable uploads with upload progress and retry support.
* A FastPix Player that streams the uploaded video.
* A workflow that waits for media processing before playback.

\


## Before you begin

* A FastPix account with an [**Access Token ID** and **Secret Key**](/docs/getting-started/activate-your-account#get-your-access-token-id-and-secret-key).
* An Astro project v7.
* Node.js.

\


## Install

From your project directory:

```bash
cd your-astro-project
npm install @fastpix/fastpix-node @fastpix/fp-astro-uploader @fastpix/fp-player
```

The [Node SDK](https://github.com/FastPix/node-sdk) talks to the FastPix API from your server, the Astro Uploader provides the upload interface, and the [FastPix Player](/docs/web-player/install-the-fastpix-web-player) plays uploaded videos.

\


### Configure Astro

Creating upload URLs and retrieving media happen on the server, so Astro must run in server mode instead of generating a fully static site.

Before configuring Astro, install the adapter that matches your deployment environment. This guide uses `@astrojs/node` for local development; see the [full list of supported adapters](https://docs.astro.build/en/guides/on-demand-rendering/) if you're deploying elsewhere:

```bash
npm install @astrojs/node
```

Update `astro.config.mjs`:

```js
import { defineConfig } from "astro/config";
import node from "@astrojs/node";

export default defineConfig({
  adapter: node({ mode: "standalone" }),
  vite: {
    optimizeDeps: {
      exclude: ["@fastpix/fp-astro-uploader"],
    },
  },
});
```

Excluding `@fastpix/fp-astro-uploader` from dependency optimization ensures the uploader's client runtime is loaded correctly.

Add your credentials to `.env`:

```bash
FASTPIX_USERNAME=your-access-token-id
FASTPIX_PASSWORD=your-secret-key
```

> **WARNING:**
> Do not prefix these with `PUBLIC_`. Astro exposes every `PUBLIC_*` variable to the browser, which would publish your Secret Key.

Create the SDK client in its own module:

```ts
// src/lib/fastpix.ts
import { Fastpix } from "@fastpix/fastpix-node";

export const fastpix = new Fastpix({
  security: {
    username: import.meta.env.FASTPIX_USERNAME,
    password: import.meta.env.FASTPIX_PASSWORD,
  },
});
```

\


## Create an upload URL

Signed upload URLs are created with your Secret Key, so this belongs on the server. An API route gives you an endpoint the browser can call:

**`src/pages/api/upload-url.ts`**

```ts
import type { APIRoute } from "astro";
import { fastpix } from "../../lib/fastpix";

export const prerender = false;

export const POST: APIRoute = async () => {
  const upload = await fastpix.inputVideo.upload(
    { corsOrigin: "*", pushMediaSettings: { accessPolicy: "public" } },
    { headers: { "X-Client-Type": "web-browser" } },
  );

  if (!("data" in upload)) throw new Error("Could not create an upload.");

  return Response.json({ url: upload.data!.url, mediaId: upload.data!.uploadId });
};
```

**`src/pages/api/upload-url.js`**

```js
import { fastpix } from "../../lib/fastpix";

export const prerender = false;

export const POST = async () => {
  const upload = await fastpix.inputVideo.upload(
    { corsOrigin: "*", pushMediaSettings: { accessPolicy: "public" } },
    { headers: { "X-Client-Type": "web-browser" } },
  );

  if (!("data" in upload)) throw new Error("Could not create an upload.");

  return Response.json({ url: upload.data.url, mediaId: upload.data.uploadId });
};
```

`export const prerender = false` is required. Without it Astro tries to run this route at build time, when there's no request to respond to.

> **NOTE:** \
> **`X-Client-Type: web-browser`** tells FastPix that a browser will perform the upload, so the signed URL is issued for browser use. Send it whenever the file is uploaded from a browser, including this setup, where your server requests the URL and the browser uploads to it.
>
> Leave it out when your own server, a CLI, or a native Android or iOS app uploads the bytes.

Keep the `mediaId`. It identifies the media everywhere else in the API, and you'll use it later to play the video. Full options are in [upload videos from device](/docs/video-on-demand-api/input-video/direct-upload-video-media), or use [create media from a URL](/docs/video-on-demand-api/input-video/create-media) to import video you already host.

\


## Add the uploader

Render `<FastPixUploader>` and give it a function that mints a URL when a file is picked:

**`src/pages/upload.astro`**

```astro
---
import { FastPixUploader } from "@fastpix/fp-astro-uploader";
---

<FastPixUploader id="up" accept="video/*" />

<script>
  import { getUploader } from "@fastpix/fp-astro-uploader/client";

  const uploader = await getUploader("#up");

  uploader.endpoint = async () => {
    const response = await fetch("/api/upload-url", { method: "POST" });
    const { url } = await response.json();
    return url;
  };
</script>
```

That's a working uploader: drag and drop, a file picker, progress, and pause, resume, and cancel controls. Styles ship with the component. There's no stylesheet to import.

You can select or drag a video into the uploader and the upload begins with visible progress.

The resolver is assigned from a `<script>` rather than passed as a prop because props are serialized when Astro renders on the server, and functions don't serialize. `getUploader` waits for the element to be defined and upgraded, then hands you the element itself; `endpoint` is a property on it.

\


### Build your own layout instead

Pass the sub-components as children and arrange them yourself. Each reads upload state from the host, so placement and order are yours:

```astro
---
import {
  FastPixUploader,
  FastPixDropZone,
  FastPixTrack,
  FastPixStatus,
  FastPixStartButton,
  FastPixPauseButton,
  FastPixResumeButton,
  FastPixAbortButton,
} from "@fastpix/fp-astro-uploader";
---

<FastPixUploader id="up" accept="video/*" autoStart={false} size="lg" appearance={{ accentColor: "#00d1ff" }}>
  <FastPixDropZone overlay>
    <p>Drag a video here, or click to browse</p>
  </FastPixDropZone>

  <FastPixStatus />
  <FastPixTrack showLabel />

  <FastPixStartButton />
  <FastPixPauseButton />
  <FastPixResumeButton />
  <FastPixAbortButton />
</FastPixUploader>
```

`autoStart={false}` holds the file until someone presses start, which is why a start button appears here and not in the default layout.

\


## Respond to upload events

The uploader emits DOM events on the element `getUploader` returns: `fastpix-upload-start`, `fastpix-progress`, `fastpix-success`, `fastpix-error`, and more:

```ts
uploader.addEventListener("fastpix-success", () => {
  /* the bytes are delivered; encoding continues */
});
```

`fastpix-success` fires when the bytes finish uploading. The media still has to be encoded before it can play, which is what the next section covers.

\


## Wait for the media to be ready

A finished upload isn't a playable video yet. It still has to be encoded. FastPix sends a `video.media.ready` webhook once playback is available.

Register your endpoint under **Org Settings** > **Webhooks** see [Set up webhooks](/docs/webhooks/set-up-webhooks), then handle the event in another API route:

**`src/pages/api/fastpix/webhook.ts`**

```ts
import type { APIRoute } from "astro";

export const prerender = false;

export const POST: APIRoute = async ({ request }) => {
  const { type, data } = await request.json();

  if (type === "video.media.ready") {
    /* data.id is the mediaId, data.playbackIds[0].id is the playback ID.
       Store them against your own record here. */
    await markVideoReady(data.id, data.playbackIds[0].id);
  }

  return Response.json({ message: "ok" });
};
```

**`src/pages/api/fastpix/webhook.js`**

```js
export const prerender = false;

export const POST = async ({ request }) => {
  const { type, data } = await request.json();

  if (type === "video.media.ready") {
    /* data.id is the mediaId, data.playbackIds[0].id is the playback ID.
       Store them against your own record here. */
    await markVideoReady(data.id, data.playbackIds[0].id);
  }

  return Response.json({ message: "ok" });
};
```

> **WARNING:** \
> **Verify the signature before trusting a payload.** Your endpoint is a public URL, so anyone who finds it could claim a video is ready. Check the `FastPix-Signature` header, an HMAC-SHA256 of the raw body, as described in [Set up webhooks](/docs/webhooks/set-up-webhooks). Read the body with `request.text()` and verify that exact string. A parsed and re-serialized body produces different bytes and never matches.

Other events, including `video.media.failed`, are in the [webhook event reference](/docs/webhooks/webhook-event-reference).

Webhooks can't reach `localhost`, so while developing either expose your dev server through a tunnel or check the status directly:

```ts
const media = await fastpix.manageVideos.get({ mediaId });
// media.data.status === "Ready"
```

\


## Play the video

Fetch the media in the page frontmatter. That runs on the server, so your credentials stay there and only the playback ID reaches the browser. `<fastpix-player>` is a web component, so it goes straight into your markup:

**`src/pages/play/[mediaId].astro`** (TypeScript)

```astro
---
import { fastpix } from "../../lib/fastpix";

export const prerender = false;

const { mediaId } = Astro.params;

/* Read the playback ID from your own database, or from FastPix like below. */
const media = await fastpix.manageVideos.get({ mediaId: mediaId! });

if (!("data" in media)) return new Response("Media not found.", { status: 404 });

const playbackId = media.data!.playbackIds![0].id!;
---

<fastpix-player playback-id={playbackId} stream-type="on-demand"/>

<script>
  import "@fastpix/fp-player";
</script>
```

**`src/pages/play/[mediaId].astro`** (JavaScript)

```astro
---
import { fastpix } from "../../lib/fastpix";

export const prerender = false;

const { mediaId } = Astro.params;

/* Read the playback ID from your own database, or from FastPix like below. */
const media = await fastpix.manageVideos.get({ mediaId });

if (!("data" in media)) return new Response("Media not found.", { status: 404 });

const playbackId = media.data.playbackIds[0].id;
---

<fastpix-player playback-id={playbackId} stream-type="on-demand"/>

<script>
  import "@fastpix/fp-player";
</script>
```

The player import lives in a `<script>`, which Astro only runs in the browser. It registers a custom element and would fail during server rendering.

Give it a size, since it fills whatever container it sits in:

```css
fastpix-player { width: 100%; aspect-ratio: 16 / 9; }
```

A playback ID isn't the same as a media ID. One media asset can carry several playback IDs with different access policies. This guide uses `accessPolicy: "public"`, so the ID alone is enough to play the video. Private and DRM playback need a signed token, covered in [play uploaded videos](/docs/web-player/play-uploaded-videos) along with autoplay, captions, and the player's full attribute and event surface.

\


## Troubleshooting

**`Cannot use server-rendered pages without an adapter`.**

API routes and pages with `export const prerender = false` run on demand. Install an adapter such as `@astrojs/node` and set it in `astro.config.mjs`.

**`getUploader: the matched element is not an <fastpix-uploader>`.**

Your script and the component are using different copies of the client runtime. Add `optimizeDeps: { exclude: ['@fastpix/fp-astro-uploader'] }` to the `vite` section of `astro.config.mjs`.

**`Invalid config: endpoint must be a non-empty string or a function`.**

The resolver was never assigned, usually because the `getUploader` call above it threw. Check the browser console for that error first.

**Uploads fail with "error code 0" after 5 attempts, but the video still processes.**

The signed URL was created without the `X-Client-Type: web-browser` header, so it isn't valid for uploads started from a browser. Add it to your upload call as shown above. The rule is about who uploads the bytes, not who requests the URL. A browser doing the upload needs the header even though your server is the one asking for the URL.

**A POST to an API route returns `403` from curl but works in the browser.**

Astro checks the `Origin` header on form-style requests. A browser sends it automatically; add `-H "Origin: http://localhost:4321"` when testing by hand.

**Chunk size is rejected.**

`chunkSize` is in KB, between 5120 and 512000, in multiples of 256.

\


## What's next

* [Web Player](https://github.com/FastPix/web-player-component), [Node SDK](https://github.com/FastPix/node-sdk), and [Web Uploads SDK](https://github.com/FastPix/web-uploads-sdk).
* [Upload videos from device](/docs/upload-videos/upload-videos-from-device), [Resumable uploads for web](/docs/upload-videos/set-up-resumable-uploads-for-web), and [Get media](/docs/video-on-demand-api/manage-videos/get-media).
* [Install the web player](/docs/web-player/install-the-fastpix-web-player), [Play uploaded videos](/docs/web-player/play-uploaded-videos), and [Webhooks](/docs/webhooks/set-up-webhooks).