> 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 React (Vite)

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 server.

A Vite application is entirely client-side, so it cannot safely store your FastPix Secret Key or receive webhooks. This guide uses a small Express server for those tasks, but you can use any backend you control.

If you'd rather not use React components, [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 Express server creates a signed upload URL using the FastPix Node SDK.
2. The React 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:

* A React 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**. See [get your Access Token ID and Secret Key](/docs/getting-started/activate-your-account#get-your-access-token-id-and-secret-key).
* A React 18 or later application created with Vite.
* Node.js.
* A backend (such as Express) to create upload URLs and receive webhooks.

\


## Install

From your project directory:

```bash
cd your-vite-project
npm install @fastpix/fp-react-uploader @fastpix/fp-player
npm install @fastpix/fastpix-node express
```

The first line covers the browser: the [React Uploader](https://github.com/FastPix/react-web-uploader) is the upload UI and the [Web Player](/docs/web-player/install-the-fastpix-web-player) plays the result. The second covers the server, where the [Node SDK](https://github.com/FastPix/node-sdk) talks to the FastPix API.

Put your credentials in `.env`:

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

> **WARNING:** \
> Do not prefix these with `VITE_`. Vite inlines every `VITE_*` variable into the client bundle, which would publish your Secret Key to every visitor. Only your server reads these.

Import the uploader stylesheet once, in `src/main.tsx`:

```tsx
import "@fastpix/fp-react-uploader/styles.css";
```

Without it the components render as an unstyled skeleton and you supply the entire look yourself. With it you get the default drop zone, progress track, status text, and buttons, which you can then adjust through the `appearance` prop or the `--fastpix-*` CSS variables, both documented in the [uploader README](https://github.com/FastPix/react-web-uploader#appearance).

\


## Create an upload URL

Signed upload URLs are created with your Secret Key, so this belongs on the server:

**`server.js`**

```js
import express from "express";
import { Fastpix } from "@fastpix/fastpix-node";

const fastpix = new Fastpix({
  security: {
    username: process.env.FASTPIX_USERNAME,
    password: process.env.FASTPIX_PASSWORD,
  },
});

const app = express();

app.post("/api/upload-url", async (_req, res) => {
  const upload = await fastpix.inputVideo.upload(
    { corsOrigin: "*", pushMediaSettings: { accessPolicy: "public" } },
    { headers: { "X-Client-Type": "web-browser" } },
  );

  res.json({ url: upload.data.url, mediaId: upload.data.uploadId });
});

app.listen(8787);
```

> **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. See [upload videos from device](/docs/upload-videos/upload-videos-from-device).

Point Vite's dev server at it so `/api` calls work from the browser without CORS setup:

```ts
// vite.config.ts
export default defineConfig({
  plugins: [react()],
  server: {
    proxy: { "/api": "http://localhost:8787" },
  },
});
```

The proxy applies to `vite dev` only. Vite builds to static files, so in production serve the API from the same origin as those assets, or enable CORS on it.

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/upload-videos/upload-videos-from-device), or use [create media from a URL](/docs/video-on-demand-api/upload-and-import-videos/create-media) to import video you already host.

\


## Add the uploader

Give `<FastPixUploader>` a function instead of a URL. It runs when a file is picked, so a URL is minted per upload rather than per page load:

**`src/Upload.tsx`**

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

async function createUpload() {
  const response = await fetch("/api/upload-url", { method: "POST" });
  const { url } = await response.json();
  return url as string;
}

export default function Upload() {
  return <FastPixUploader endpoint={createUpload} accept="video/*" />;
}
```

**`src/Upload.jsx`**

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

async function createUpload() {
  const response = await fetch("/api/upload-url", { method: "POST" });
  const { url } = await response.json();
  return url;
}

export default function Upload() {
  return <FastPixUploader endpoint={createUpload} accept="video/*" />;
}
```

That's a working uploader: drag and drop, a file picker, progress, and pause, resume, and cancel controls.

You can drag and drop a video, and the upload starts with visible progress.

\


### Build your own layout instead

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

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

<FastPixUploader endpoint={createUpload} 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.

Tracking progress yourself, driving the uploader from a `ref`, reading live state with `useUploaderContext()`, and building a fully headless UI are covered in the [uploader README](https://github.com/FastPix/react-web-uploader).

\


## Respond to upload events

The uploader reports its lifecycle through callback props: `onProgress`, `onSuccess`, `onError`, and more. All are optional:

```tsx
<FastPixUploader
  endpoint={createUpload}
  accept="video/*"
  onProgress={(percent) => console.log(percent)}
  onSuccess={() => {
    /* the bytes are delivered; encoding continues */
  }}
  onError={(error) => console.error(error.message)}
/>
```

`onSuccess` fires when the bytes finish uploading. The media still has to be encoded before it can play, which is what the next section covers. The full callback list is in the [uploader README](https://github.com/FastPix/react-web-uploader#events).

\


## 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 on the same server:

**`server.js`**

```js
app.post("/api/fastpix/webhook", express.json(), async (req, res) => {
  const { type, data } = req.body;

  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);
  }

  res.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). Capture the raw body with `express.raw({ type: "application/json" })` and verify those exact bytes. 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 server through a tunnel or check the status directly:

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

\


## Play the video

`<fastpix-player>` is a web component, so it works in React as a plain tag:

**`src/Player.tsx`**

```tsx
import "@fastpix/fp-player";

export default function Player({ playbackId }: { playbackId: string }) {
  return <fastpix-player playback-id={playbackId} stream-type="on-demand" />;
}
```

**`src/Player.jsx`**

```jsx
import "@fastpix/fp-player";

export default function Player({ playbackId }) {
  return <fastpix-player playback-id={playbackId} stream-type="on-demand" />;
}
```

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

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

Expose the playback ID your webhook stored, so the browser can read it without your credentials:

**`server.js`**

```js
app.get("/api/media/:mediaId", async (req, res) => {
  const media = await fastpix.manageVideos.get({ mediaId: req.params.mediaId });

  res.json({
    status: media.data.status,
    playbackId: media.data.playbackIds?.[0]?.id ?? null,
  });
});
```

Then fetch it and render the player:

**`src/Watch.tsx`**

```tsx
import { useEffect, useState } from "react";
import Player from "./Player";

export default function Watch({ mediaId }: { mediaId: string }) {
  const [playbackId, setPlaybackId] = useState<string | null>(null);

  useEffect(() => {
    fetch(`/api/media/${mediaId}`)
      .then((response) => response.json())
      .then(({ playbackId }) => setPlaybackId(playbackId));
  }, [mediaId]);

  if (!playbackId) return null;

  return <Player playbackId={playbackId} />;
}
```

**`src/Watch.jsx`**

```jsx
import { useEffect, useState } from "react";
import Player from "./Player";

export default function Watch({ mediaId }) {
  const [playbackId, setPlaybackId] = useState(null);

  useEffect(() => {
    fetch(`/api/media/${mediaId}`)
      .then((response) => response.json())
      .then(({ playbackId }) => setPlaybackId(playbackId));
  }, [mediaId]);

  if (!playbackId) return null;

  return <Player playbackId={playbackId} />;
}
```

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

**`TS2882: Cannot find module or type declarations for side-effect import of '@fastpix/fp-react-uploader/styles.css'`**

TypeScript has no declarations for CSS imports. Add Vite's client types to `tsconfig.app.json`:

```json
{ "compilerOptions": { "types": ["vite/client"] } }
```

Older templates use `src/vite-env.d.ts` containing `/// <reference types="vite/client" />` instead. If that file was deleted, restore it. The same error appears for your own `./index.css`, which is a quick way to confirm the cause. Note that `vite build` alone doesn't typecheck; the error only surfaces through `npm run build`, which runs `tsc -b` 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 callback on the uploader reads stale state.**

Callbacks such as `onSuccess` are captured when the upload starts, so anything they read reflects that moment. Have the callback set a flag, and react to it from an effect that reads current state.

**`/api` requests 404 in dev.**

The Vite proxy is missing, or your API server isn't running. Both `vite dev` and `node server.js` need to be running.

**`/api` requests 404 in production.**

`vite build` emits static files only, so anything mounted on Vite's dev server is gone. Deploy the API separately and serve it from the same origin, or enable CORS on it.

**Chunk size is rejected.**

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

\


## What's next

* [React Uploader](https://github.com/FastPix/react-web-uploader), [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).