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

# Set up resumable uploads for React Native

The FastPix [React Native Uploads SDK](https://github.com/FastPix/react-native-uploader) adds resumable, chunked uploads to your React Native app. It uploads large videos in chunks, retries failed chunks on its own, and resumes from the last confirmed byte after a pause or a dropped connection, so an interrupted upload continues instead of starting over. It works on both Android and iOS.

Uploads go straight from the device to FastPix, so the video bytes never pass through your own server.

\


#### How resumable uploads work through chunking

\


A resumable upload splits a large file into smaller pieces, called **chunks**, and uploads them one at a time:

1. **Split the file.** The file is divided into smaller chunks (5 MB by default).
2. **Upload each chunk.** Each chunk is sent separately. The server confirms each one.
3. **Resume from the last chunk.** If the upload is interrupted, it continues from the last confirmed chunk instead of starting over.

\


This matters because:

* **It lowers the risk.** If a chunk fails on a flaky network, only that chunk is retried, not the whole file.
* **It performs better.** Smaller chunks transfer more reliably on slow or mobile connections.
* **It is easier to manage.** Chunk-by-chunk progress makes features like pause, resume, and progress bars straightforward.

\
\


## Platform support

| Platform     | Minimum version      |
| ------------ | -------------------- |
| Android      | API 21 (Android 5.0) |
| iOS          | iOS 13.0             |
| React Native | 0.70+                |

\


## Install the SDK

Install the package with npm or your preferred package manager:

```bash
npm install @fastpix/react-native-uploads
```

The SDK bundles its runtime dependencies (`@react-native-community/netinfo`, `react-native-blob-util`, and `axios`), so there are no peer dependencies to install manually.

\


**iOS:** the native modules auto-link on React Native 0.70+. After installing the package, install the pods:

```bash
cd ios && pod install && cd ..
```

**Android:** no manual linking or extra setup is required. The native modules auto-link.

> **Note:** The SDK depends on native modules, so it does not run in [Expo Go](https://expo.dev/go). In an Expo project, use a [development build](https://docs.expo.dev/develop/development-builds/introduction/) (`npx expo run:ios` or `npx expo run:android`). For a full Expo walkthrough, see [Upload videos from a React Native app](/docs/frameworks/react-native-uploads).

\


## Generate a signed upload URL

The SDK uploads to a **signed URL**: a pre-authorized, short-lived URL that lets the device upload directly to FastPix without exposing your API credentials in the app.

To create one, you need a valid **Access Token** and **Secret Key**. See the [Basic Authentication guide](/docs/getting-started/activate-your-account#access-token-permissions) for how to retrieve them. Then call the [Upload media from device](/docs/video-on-demand-api/upload-and-import-videos/direct-upload-video-media) API to get the signed URL.

> **Never ship your Access Token or Secret Key in the app bundle.** Create signed URLs on your backend and return only the signed URL to the device.

The service below calls the Direct Upload API and returns a signed URL. This is **your** backend code, not part of the SDK. It uses `axios` and `base-64` for the Basic Auth header (`npm install axios base-64`); on a Node backend you can drop `base-64` and use `Buffer.from(...).toString("base64")` instead.

```javascript
import axios from "axios";
import base64 from "base-64";

const TOKEN_ID = "your_token_id";
const SECRET_KEY = "your_secret_key";
const API_BASE_URL = "https://api.fastpix.com/v1/on-demand";

export async function generateSignedUrl(metadata = { uploadedBy: "react_native_app" }) {
  const auth = `Basic ${base64.encode(`${TOKEN_ID}:${SECRET_KEY}`)}`;

  const body = {
    corsOrigin: "*",
    pushMediaSettings: {
      metadata,
      accessPolicy: "public",
      maxResolution: "1080p",
    },
  };

  const response = await axios.post(`${API_BASE_URL}/upload`, body, {
    headers: {
      "Content-Type": "application/json",
      Accept: "application/json",
      Authorization: auth,
    },
  });

  const data = response.data?.data;
  if (!data?.url || !data?.uploadId) {
    throw new Error("Failed to generate signed URL");
  }

  // { url, uploadId } — pass `url` to FastPixUpload; keep `uploadId` for tracking
  return { url: data.url, uploadId: data.uploadId };
}
```

\


## Upload a file

Create a `FastPixUpload` with the signed URL and the local file URI, then call `start()`:

```javascript
import { FastPixUpload } from "@fastpix/react-native-uploads";

const upload = new FastPixUpload({
  endpoint: "https://storage.googleapis.com/...your-signed-url...", // the signed URL
  fileUri: asset.uri, // file:// URI from your image or video picker
  chunkSize: 5 * 1024, // 5 MB (minimum allowed)
});

await upload.start();
```

Because `endpoint` also accepts an **async factory** (`() => Promise<string>`), you can plug the signed-URL generator in directly. The factory runs once when `start()` is called, so the URL is created fresh and your token stays on your backend:

```javascript
import { FastPixUpload } from "@fastpix/react-native-uploads";
import { generateSignedUrl } from "./services/SignedUrlService";

export async function uploadVideo(fileUri) {
  const upload = new FastPixUpload({
    endpoint: async () => (await generateSignedUrl()).url,
    fileUri,
    chunkSize: 16 * 1024, // 16 MB chunks
    maxRetries: 3,
    retryDelay: 2000,
    maxFileSize: 2 * 1024 * 1024, // 2 GB limit (in KB); 0 = no limit
    enableLogs: __DEV__,
  });

  upload.on("progress", ({ percentage }) => console.log(`${percentage}%`));
  upload.on("success", () => console.log("Upload complete"));
  upload.on("error", ({ message }) => console.error(message));

  await upload.start();
  return upload; // keep the ref to pause(), resume(), or abort()
}
```

\


### Configuration parameters

The `FastPixUpload` constructor accepts these options:

| Name          | Type                                | Required | Description                                                                                                                                                                   |
| ------------- | ----------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `endpoint`    | `string` or `() => Promise<string>` | Required | The signed FastPix upload URL, or an async factory that returns one. The factory runs once at `start()`, which is useful when tokens are short-lived.                         |
| `fileUri`     | `string`                            | Required | Local file path from your file picker. Accepts `file://` URIs and plain filesystem paths.                                                                                     |
| `chunkSize`   | `number` (in KB)                    | Optional | Size of each chunk in kilobytes. Default is `5120` KB (5 MB). Minimum 5120 KB (5 MB), maximum 512000 KB (500 MB). Must be a multiple of 256, so any `N * 1024` value is safe. |
| `maxRetries`  | `number`                            | Optional | Retry attempts per failed chunk before the upload fails. Default is `5`.                                                                                                      |
| `retryDelay`  | `number` (in ms)                    | Optional | Delay before the first retry. Each later retry doubles the delay (exponential back-off). Default is `1000`.                                                                   |
| `maxFileSize` | `number` (in KB)                    | Optional | Maximum allowed file size. `0` means no limit. Larger files fail before any network request. Default is `0`.                                                                  |
| `enableLogs`  | `boolean`                           | Optional | Enable SDK debug logging to the console. Use in development, disable in production. Default is `false`.                                                                       |

\


## Pause, resume, and network recovery

Every chunk that finishes is confirmed by the server, so a paused or interrupted upload always continues from the **last confirmed byte**. Completed chunks are never re-sent.

An upload can pause in two ways:

| Trigger     | How it happens                                                              | How it resumes                                              |
| ----------- | --------------------------------------------------------------------------- | ----------------------------------------------------------- |
| **User**    | You call `upload.pause()`                                                   | You call `await upload.resume()`                            |
| **Network** | Connectivity is lost, or the connection switches between Wi-Fi and cellular | The SDK resumes **automatically** when connectivity returns |

Both cases emit a `pause` event with a `reason` (`'user'` or `'network'`), so your UI can respond:

```javascript
upload.on("progress", ({ percentage }) => console.log(`${percentage}%`));
upload.on("pause", ({ reason }) => console.log(`Paused (${reason})`));
upload.on("resume", ({ fromOffset }) => console.log(`Resumed from byte ${fromOffset}`));
upload.on("success", () => console.log("Upload complete"));

await upload.start();

// Later, from a button:
upload.pause();        // pauses immediately, keeping progress
await upload.resume(); // re-syncs the server offset, then continues
```

An upload you paused (`reason: 'user'`) is **not** auto-resumed on reconnect. That stays under your control, so a user-paused upload never restarts on its own.

\


## Chunk-level retry tracking

Retries are tracked **per chunk**, not with one global counter. Each chunk gets its own budget of `maxRetries` attempts with exponential back-off, so one flaky chunk cannot use up the retry allowance of the others. On a network blip, only the in-flight chunk retries, and completed chunks are never re-uploaded.

Watch it live through the chunk events:

```javascript
upload.on("chunkAttempt", ({ chunkIndex, attemptNumber, totalChunkNumbers }) => {
  console.log(`Chunk ${chunkIndex}/${totalChunkNumbers} — attempt ${attemptNumber}`);
});

upload.on("chunkAttemptFailure", ({ chunkIndex, attemptNumber, error }) => {
  console.warn(`Chunk ${chunkIndex} failed (attempt ${attemptNumber}): ${error.message}`);
});

upload.on("chunkSuccess", ({ chunkIndex }) => {
  console.log(`Chunk ${chunkIndex} uploaded`);
});
```

\


## Lifecycle events

Subscribe with `upload.on(event, handler)`. Each subscription returns an unsubscribe function, which makes cleanup in React's `useEffect` straightforward.

| Event                 | Description                                        |
| --------------------- | -------------------------------------------------- |
| `started`             | The upload starts.                                 |
| `progress`            | Reports progress, uploaded bytes, and total bytes. |
| `stateChange`         | The upload state changes (`from`, `to`).           |
| `chunkAttempt`        | A chunk upload is attempted.                       |
| `chunkAttemptFailure` | A chunk attempt fails and will be retried.         |
| `chunkSuccess`        | A chunk uploads successfully.                      |
| `success`             | The upload completes.                              |
| `error`               | The upload fails with a non-recoverable error.     |
| `pause`               | The upload is paused (carries `reason`).           |
| `resume`              | The upload resumes (carries `fromOffset`).         |
| `abort`               | The upload is cancelled.                           |
| `offline`             | Network connectivity is lost.                      |
| `online`              | Network connectivity is restored.                  |

\


## Control methods

| Method                | Signature                        | Description                                                                                  |
| --------------------- | -------------------------------- | -------------------------------------------------------------------------------------------- |
| `start()`             | `() => Promise<void>`            | Starts the upload. Valid only from the `IDLE` state.                                         |
| `pause()`             | `() => void`                     | Pauses an active upload, keeping the last confirmed offset. Valid only from `UPLOADING`.     |
| `resume()`            | `() => Promise<void>`            | Resumes a paused upload. Re-syncs the server offset first. Valid only from `PAUSED`.         |
| `abort()`             | `() => void`                     | Cancels the upload and releases native resources. Emits `abort`, then removes all listeners. |
| `on(event, handler)`  | `(event, handler) => () => void` | Subscribes to a lifecycle event. Returns an unsubscribe function.                            |
| `off(event, handler)` | `(event, handler) => void`       | Removes a previously registered listener.                                                    |

The SDK also exposes `state`, `progress`, and `stateHistory` getters. The upload moves through these states:

```typescript
type UploadState =
  | "IDLE"       // constructed, not started
  | "STARTED"    // start() called, preparing
  | "UPLOADING"  // actively transferring chunks
  | "PAUSED"     // paused (by user or network)
  | "RESUMED"    // transitioning back into UPLOADING
  | "FAILED"     // stopped with a non-recoverable error
  | "COMPLETED"; // all chunks uploaded
```

\


## Full example: progress bar with pause, resume, and abort

A complete component wiring the lifecycle to UI controls:

```jsx
import React, { useEffect, useRef, useState } from "react";
import { View, Text, Button, ActivityIndicator } from "react-native";
import { FastPixUpload } from "@fastpix/react-native-uploads";
import { generateSignedUrl } from "./services/SignedUrlService";

export function VideoUploader({ fileUri }) {
  const uploadRef = useRef(null);
  const [percentage, setPercentage] = useState(0);
  const [state, setState] = useState("IDLE");

  useEffect(() => {
    const upload = new FastPixUpload({
      endpoint: async () => (await generateSignedUrl()).url, // created lazily at start()
      fileUri,
      chunkSize: 16 * 1024, // 16 MB chunks
      maxRetries: 3,
      retryDelay: 2000,
      enableLogs: __DEV__,
    });
    uploadRef.current = upload;

    // on() returns an unsubscribe function — collect and clean up on unmount.
    const off = [
      upload.on("progress", ({ percentage }) => setPercentage(percentage)),
      upload.on("stateChange", ({ to }) => setState(to)),
      upload.on("pause", ({ reason }) =>
        console.log(reason === "network" ? "Paused — waiting for network" : "Paused by user"),
      ),
      upload.on("resume", ({ fromOffset }) => console.log(`Resumed from ${fromOffset}`)),
      upload.on("success", () => console.log("Upload complete")),
      upload.on("error", ({ message }) => console.error(message)),
    ];

    upload.start();

    return () => {
      off.forEach((unsubscribe) => unsubscribe());
      upload.abort(); // release native resources if the screen unmounts mid-upload
    };
  }, [fileUri]);

  const isUploading = state === "UPLOADING" || state === "RESUMED";
  const isPaused = state === "PAUSED";

  return (
    <View style={{ padding: 16, gap: 12 }}>
      <Text>{state} — {percentage}%</Text>
      {isUploading && <ActivityIndicator />}

      <Button title="Pause"  onPress={() => uploadRef.current?.pause()}  disabled={!isUploading} />
      <Button title="Resume" onPress={() => uploadRef.current?.resume()} disabled={!isPaused} />
      <Button title="Abort"  onPress={() => uploadRef.current?.abort()}  disabled={state === "IDLE"} />
    </View>
  );
}
```

\


## Troubleshooting

| Symptom                                          | Likely cause                                                                                     | Fix                                                                                                               |
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
| `File is empty or could not be read`             | `fileUri` points to a missing file, or a `content://` or asset URI the native layer cannot read. | Pass a resolved `file://` path or plain filesystem path. Copy picker or asset URIs to a local file first.         |
| `File size exceeds the maximum allowed size`     | The file is larger than `maxFileSize` (in KB).                                                   | Increase `maxFileSize`, or set it to `0` to disable the limit.                                                    |
| `chunkSize` validation error                     | `chunkSize` is outside the allowed range, or not a multiple of 256.                              | Use a value between `5120` KB (5 MB) and `512000` KB (500 MB) that is divisible by 256 (for example `16 * 1024`). |
| Upload never starts, or `start()` is ignored     | `start()` was called when the upload was not in the `IDLE` state.                                | Only call `start()` from `IDLE`. Use `resume()` to continue a paused upload.                                      |
| Upload stalls after switching Wi-Fi and cellular | The in-flight socket died without an offline event.                                              | No action needed. The SDK detects the switch and resumes from the confirmed offset.                               |
| iOS build cannot find native modules             | Pods not installed after adding the package.                                                     | Run `cd ios && pod install`.                                                                                      |
| No events firing                                 | Listeners were attached after `abort()`, which removes all listeners.                            | Re-attach listeners on a new `FastPixUpload` instance after an abort.                                             |

Set `enableLogs: true` in the constructor to see detailed SDK logs while diagnosing issues, and disable it in production.

\


#### Frequently asked questions

A resumable upload is a way to transfer a file that lets you pause, or recover from a network drop or interruption, without starting over from the beginning. The file is sent in sequential chunks, the server tracks how many bytes it has confirmed, and the upload continues from that last confirmed offset. It is the same pattern used by standards like the open-source tus protocol, and it is what makes large uploads reliable on mobile networks.

While the app is running, the SDK recovers on its own: it pauses when connectivity drops or the connection switches between Wi-Fi and cellular, then resumes from the last confirmed byte when the network returns. It does not perform OS-level background uploading after the app is suspended or terminated, so keep the upload screen in the foreground until it finishes.

A signed upload URL is a short-lived, pre-authorized URL that lets a device upload directly to FastPix without carrying your Access Token or Secret Key. You create it on your backend with the Direct Upload API and return only the URL to the app, so your credentials never ship in the app bundle.

No. The SDK depends on native modules, which Expo Go does not include, so you get a "native module that doesn't exist" error. Use an Expo development build (`npx expo run:ios` or `npx expo run:android`) to run uploads on a device.

Chunk size must be between 5 MB and 500 MB and a multiple of 256 KB, so any `N * 1024` value works. The default is 5 MB. `maxFileSize` is optional and in KB; set it to `0` for no limit.

\


## What's next

* [Upload videos from a React Native app](/docs/frameworks/react-native-uploads) for a full Expo walkthrough with a video picker and playback.
* [Set up resumable uploads for Web](/docs/upload-videos/set-up-resumable-uploads-for-web) for the browser SDK.
* [Play your videos](/docs/video-on-demand/play-your-videos) once FastPix has processed the upload.