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

# Upload videos from a React Native app

Uploading video from a phone is different from uploading other files. Connections drop, apps get backgrounded, and networks switch between Wi-Fi and cellular mid-transfer. A plain `fetch` of a whole file starts over every time that happens.

The [FastPix React Native Uploads SDK](https://github.com/FastPix/react-native-uploader) uploads in chunks and resumes from the last confirmed byte, so an interrupted upload continues instead of restarting. Uploads go directly from the device to FastPix, so the video bytes never pass through your server.

This guide continues from the [React Native quickstart](/docs/frameworks/react-native-quickstart) app. By the end you can pick a video, upload it with progress and pause and resume, and play it back once FastPix has processed it.

> **WARNING:** \
> The Uploads SDK depends on native modules, so uploading does not run in [Expo Go](https://expo.dev/go). You get a "native module that doesn't exist" error. Run a [development build](https://docs.expo.dev/develop/development-builds/introduction/) (`npx expo run:ios` or `npx expo run:android`, or EAS Build) to try it on a device. Playback (the quickstart) still works in Expo Go, and only the upload screen needs the development build.

\


## How it works

1. Your backend creates a **signed upload URL** using the FastPix Node SDK and your Secret Key.
2. The app lets the user pick a video with `expo-image-picker`.
3. The React Native Uploads SDK uploads the file to that URL in chunks, emitting progress events.
4. FastPix processes the upload asynchronously and assigns it a **playback ID**.
5. Once ready, you play it with the `VideoPlayer` from the quickstart.

\


## What you'll build

* A screen with a "Pick a video" button.
* A progress bar with working **Pause** and **Resume** controls.
* Playback of the uploaded video once it's ready.

\


## Before you begin

Make sure you have the following:

* The [quickstart](/docs/frameworks/react-native-quickstart) app, or any Expo app with a `VideoPlayer` component.
* 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#generate-api-credentials).
* A backend you control (this guide uses a small Express server) to create signed upload URLs.
* A development build to run the upload screen. The Uploads SDK uses native modules that don't run in Expo Go.

> **WARNING:** \
> Your Secret Key must never ship inside the app. Anyone can extract strings from a mobile binary. Create signed upload URLs on a server and have the app request them.

Each code block is a complete file. The bold filename above it tells you where the code goes. Create or replace that file with the code shown.

\


## Create a signed upload URL (backend)

Signed upload URLs are created with your Secret Key, so this belongs on the server. Here's a minimal Express route:

**`server.js`**

```js
import express from "express";
import Client from "@fastpix/fastpix-node";

const fastpix = new Client({
  accessTokenId: process.env.FASTPIX_USERNAME, // Access Token ID
  secretKey: process.env.FASTPIX_PASSWORD, // Secret Key
});

const app = express();

// Don't advertise the framework/version in the X-Powered-By header.
app.disable("x-powered-by");

app.post("/uploads", async (_req, res) => {
  const result = await fastpix.uploadMediaFromDevice({
    corsOrigin: "*",
    pushMediaSettings: { accessPolicy: "public" },
  });
  res.json({ uploadUrl: result.data.url, uploadId: result.data.uploadId });
});

app.listen(8787);
```

Install the server dependencies and set your credentials:

```bash
npm install @fastpix/fastpix-node express
export FASTPIX_USERNAME=your-access-token-id
export FASTPIX_PASSWORD=your-secret-key
```

> **NOTE:** \
> Unlike a browser upload, a native device upload does not need the `X-Client-Type: web-browser` header. The signed URL from the call above is ready for the React Native SDK to upload to directly.

Keep the `uploadId`. It identifies the media in the rest of the API, and you use it to fetch the playback ID once processing finishes.

\


## Install the app dependencies

In your Expo app:

```bash
npx expo install @fastpix/react-native-uploads expo-image-picker
```

`expo-image-picker` opens the system library so the user can choose a video. The [Uploads SDK](https://github.com/FastPix/react-native-uploader) handles the chunked, resumable transfer.

> **NOTE:** \
> If you use a bare React Native project (not managed Expo), run `cd ios && pod install` after installing so the native modules link. Managed Expo development builds handle this during prebuild.

\


## Pick a video

Start with a component that opens the picker and shows the chosen file. Point `CREATE_UPLOAD_ENDPOINT` at your backend route:

**`src/components/upload-video.tsx`**

```tsx
import * as ImagePicker from "expo-image-picker";
import { useState } from "react";
import { Button, StyleSheet, Text, View } from "react-native";

// URL of your backend route that creates a FastPix signed upload URL.
const CREATE_UPLOAD_ENDPOINT = "https://your-backend.example.com/uploads";

export default function UploadVideo() {
  const [fileUri, setFileUri] = useState<string | null>(null);

  async function pickVideo() {
    const result = await ImagePicker.launchImageLibraryAsync({
      mediaTypes: "videos",
    });
    if (result.canceled) return;
    setFileUri(result.assets[0].uri);
  }

  return (
    <View style={styles.container}>
      <Button title="Pick a video to upload" onPress={pickVideo} />
      {fileUri && <Text style={styles.label}>Selected: {fileUri}</Text>}
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: "center", padding: 24, gap: 16 },
  label: { fontSize: 12, textAlign: "center" },
});
```

\


## Upload with progress, pause, and resume

Now wire the picked file into `FastPixUpload`. Three things to know about the API:

* **`endpoint` takes an async function**, not a URL. The SDK calls it to mint a fresh signed URL right when the upload begins, so URLs aren't created for videos the user never uploads.
* **`chunkSize` is in KB and must be a multiple of 256 KB.** `5 * 1024` is a 5 MB chunk (20 x 256 KB), a good default for mobile. Smaller chunks resume faster on flaky networks but add per-chunk overhead. Keep any custom size 256 KB-aligned, or a mid-upload chunk fails with `HTTP 400`.
* **The upload emits events.** Subscribe with `upload.on(...)` to drive a progress bar, and keep a `ref` to it so buttons can `pause()` and `resume()`.

This complete component replaces the placeholder from the previous step:

**`src/components/upload-video.tsx`**

```tsx
import { FastPixUpload } from "@fastpix/react-native-uploads";
import * as ImagePicker from "expo-image-picker";
import { useRef, useState } from "react";
import { Button, StyleSheet, Text, View } from "react-native";

const CREATE_UPLOAD_ENDPOINT = "https://your-backend.example.com/uploads";

type UploadStatus = "idle" | "uploading" | "paused" | "success" | "error";

async function createUploadUrl(): Promise<string> {
  const response = await fetch(CREATE_UPLOAD_ENDPOINT, { method: "POST" });
  if (!response.ok) throw new Error("Could not create an upload URL");
  const { uploadUrl } = (await response.json()) as { uploadUrl: string };
  return uploadUrl;
}

export default function UploadVideo() {
  const uploadRef = useRef<FastPixUpload | null>(null);
  const [status, setStatus] = useState<UploadStatus>("idle");
  const [percentage, setPercentage] = useState(0);
  const [error, setError] = useState<string | null>(null);

  async function pickAndUpload() {
    const result = await ImagePicker.launchImageLibraryAsync({
      mediaTypes: "videos",
    });
    if (result.canceled) return;

    const upload = new FastPixUpload({
      endpoint: createUploadUrl,
      fileUri: result.assets[0].uri,
      chunkSize: 5 * 1024,
    });
    uploadRef.current = upload;

    upload.on("progress", ({ percentage }) => setPercentage(percentage));
    upload.on("success", () => setStatus("success"));
    upload.on("error", ({ message }) => {
      setError(message);
      setStatus("error");
    });

    setError(null);
    setStatus("uploading");
    await upload.start();
  }

  function togglePause() {
    const upload = uploadRef.current;
    if (!upload) return;
    if (status === "uploading") {
      upload.pause();
      setStatus("paused");
    } else if (status === "paused") {
      upload.resume();
      setStatus("uploading");
    }
  }

  return (
    <View style={styles.container}>
      <Button title="Pick a video to upload" onPress={pickAndUpload} />
      {status !== "idle" && (
        <View style={styles.progress}>
          <Text style={styles.label}>
            {status === "success"
              ? "Upload complete"
              : status === "error"
                ? `Failed: ${error}`
                : `${percentage}%`}
          </Text>
          <View style={styles.track}>
            <View style={[styles.fill, { width: `${percentage}%` }]} />
          </View>
          {(status === "uploading" || status === "paused") && (
            <Button
              title={status === "paused" ? "Resume" : "Pause"}
              onPress={togglePause}
            />
          )}
        </View>
      )}
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: "center", padding: 24, gap: 16 },
  progress: { gap: 8 },
  label: { fontSize: 16, textAlign: "center" },
  track: {
    height: 8,
    borderRadius: 4,
    backgroundColor: "#e0e0e0",
    overflow: "hidden",
  },
  fill: { height: "100%", backgroundColor: "#0a84ff" },
});
```

> **NOTE:** \
> The SDK also resumes on its own when the network drops and comes back, and when the device switches between Wi-Fi and cellular. `pause()` and `resume()` are for user-driven control on top of that. Other events you can subscribe to include `stateChange`, `pause`, and `resume`.

\


## Play the uploaded video

After `success`, FastPix processes the video before it can be streamed. Once it's ready, fetch the playback ID for your `uploadId` from your backend and hand it to the quickstart player:

**`src/app/watch.tsx`**

```tsx
import VideoPlayer from "../components/video-player";

export default function Watch({ playbackId }: { readonly playbackId: string }) {
  return <VideoPlayer playbackId={playbackId} />;
}
```

> **NOTE:** \
> Processing is asynchronous, so the playback ID isn't available the instant the upload finishes. The reliable signal is the [`video.media.ready` webhook](/docs/webhooks/set-up-webhooks) to your backend, and you can also poll [get media](/docs/video-on-demand-api/manage-videos/get-media) by `uploadId`.

\


## Troubleshooting

**The upload fails immediately with a URL or credentials error.**

Your backend route isn't returning a valid signed URL. Test it directly with `curl -X POST https://your-backend.example.com/uploads`. It should return JSON with an `uploadUrl`. Check that `FASTPIX_USERNAME` and `FASTPIX_PASSWORD` are set on the server.

**The picker returns a URI that the SDK can't read.**

`expo-image-picker` returns a `file://` URI. Pass `result.assets[0].uri` straight through as `fileUri`, and don't strip the scheme. On Android, make sure the app has media permission (the picker requests it on first use).

**The chunk size is rejected, or a mid-upload chunk fails with `HTTP 400`.**

`chunkSize` is in KB, and every chunk except the last must be a **multiple of 256 KB**. This is a hard requirement of the underlying resumable-upload storage, not FastPix-specific. `5 * 1024` (5 MB, which is 20 x 256 KB) is a safe default and what this guide uses. If you set a custom size, keep it 256 KB-aligned (`n * 256`). A non-aligned value uploads the first chunk fine but fails a later one with a `400`. Also stay within the valid range (roughly 5 MB to 500 MB per chunk).

**Every chunk fails with `HTTP 400`.**

Your backend is sending the `X-Client-Type: web-browser` header when it creates the upload URL. That header is for browser uploaders (which perform a resumable-session handshake). The React Native uploader `PUT`s chunks directly, so it needs the plain device upload URL you get by **omitting** that header. Remove it from the backend call and the chunks upload correctly.

**Nothing happens after `pod install` on a bare project.**

Rebuild the native app (`npx expo run:ios`, or open Xcode and run). Installing pods alone doesn't rebuild an already-installed app.

\


#### Frequently asked questions

No. Uploads are authorized by a signed upload URL created with your Secret Key, and the Secret Key must never ship inside a mobile app, since anyone can extract strings from the binary. Create the signed URL on a server you control (this guide uses a small Express route) and have the app request it.

No. The Uploads SDK depends on native modules that aren't in Expo Go, so the upload screen throws a "native module that doesn't exist" error there. Run a development build with `npx expo run:ios` or `npx expo run:android` (or EAS Build). Playback from the quickstart still works in Expo Go; only uploading needs the development build.

Processing is asynchronous, so the playback ID isn't ready the instant the upload finishes. Listen for the [`video.media.ready` webhook](/docs/webhooks/set-up-webhooks) on your backend, or poll [get media](/docs/video-on-demand-api/manage-videos/get-media) by `uploadId` until its status is ready, then use the playback ID with the player.

\


## What's next

* [Play a FastPix video in React Native](/docs/frameworks/react-native-quickstart) and [Advanced playback in React Native](/docs/frameworks/react-native-advanced-playback).
* [React Native Uploads SDK](https://github.com/FastPix/react-native-uploader) and [Node SDK](https://github.com/FastPix/node-sdk).
* [Set up webhooks](/docs/webhooks/set-up-webhooks) and [Get media](/docs/video-on-demand-api/manage-videos/get-media) to know when processing finishes.