# Facebook Video Upload API: Page Videos and Reels

How the Facebook video upload API works for Pages: resumable uploads, the three-phase Reels flow, status polling, Reels specs, and the 30-per-day limit.

Canonical page: https://www.tryadeli.com/blog/facebook-video-upload-api
Last updated: 2026-10-19

By Mika Reyes, Co-founder, Adeli. Published 2026-10-19.

## The short answer

Facebook has two video upload flows for Pages. Reels use the video_reels edge in three phases: start a session, upload the file to rupload.facebook.com, then finish with video_state PUBLISHED and poll the video's status. Feed videos use the Resumable Upload API, then publish the file handle to the Page's videos edge. Both need pages_manage_posts. Adeli uploads both with one request and a status endpoint.

The Facebook video upload API is **two separate flows**, both part of Meta's [Video API](https://developers.facebook.com/docs/video-api/overview). **Reels** go through the Page's `video_reels` edge in three phases, with the file sent to `rupload.facebook.com`. **Feed videos** go through the **Resumable Upload API**, then get published to the Page's `videos` edge. Both need a Page access token with `pages_manage_posts`. The calls below are copied from Meta's docs, followed by the status fields to poll and the Reels specs.

This post covers video only. For text, link, and photo posts, see [how to post to a Facebook Page with the API](https://www.tryadeli.com/blog/post-to-facebook-via-api).

## Which Facebook video upload flow do you need?

**Pick by what the video should become on the Page.** The two flows use different endpoints and hosts:

| | Reel | Feed video |
|---|---|---|
| **Endpoint** | `POST /{page-id}/video_reels` | `POST /{page-id}/videos` |
| **Upload host** | `rupload.facebook.com` | `graph.facebook.com` (Resumable Upload API) |
| **Phases** | start, upload, finish | start session, upload, publish |
| **Length** | 3 to 90 seconds | Not stated in Meta's Video API docs |
| **Aspect ratio** | 9:16 | Any |
| **Daily limit** | 30 API-published Reels per Page per 24 hours | Not stated in Meta's Video API docs |
| **Scheduling window** | 10 minutes to 29 days | 10 minutes to 6 months |

Sources: Meta's [Reels Publishing guide](https://developers.facebook.com/docs/video-api/guides/reels-publishing) and [Page videos reference](https://developers.facebook.com/docs/graph-api/reference/page/videos/).

Reels can only be published to Facebook **Pages**. Personal profiles and Groups have no video publishing API.

## What do you need before you start?

Both flows need the same Page setup:

- **A Page access token** from someone who can perform the `CREATE_CONTENT` task on the Page. Our [Facebook Page access token guide](https://www.tryadeli.com/blog/facebook-page-access-token) shows how to get one that doesn't expire.
- **The permissions:** `pages_show_list`, `pages_read_engagement`, and `pages_manage_posts`, granted through Facebook Login.
- **Advanced Access** for those permissions if the Page belongs to someone without a role on your app. That means App Review. The [Facebook Graph API guide](https://www.tryadeli.com/blog/facebook-graph-api) covers how access works.
- **For feed videos only:** your app ID and a **user access token**, because the Resumable Upload API opens its session on the app, not the Page.

The examples use API version v26.0, as Meta's docs do.

## Step 1: Start a Reels upload session

**POST to the Page's `video_reels` edge with `upload_phase=start`.** Meta returns a video ID and the URL to send the file to:

```bash
curl -X POST "https://graph.facebook.com/v26.0/$PAGE_ID/video_reels" \
  -H "Content-Type: application/json" \
  -d '{"upload_phase":"start","access_token":"'"$PAGE_TOKEN"'"}'
```

```json
{
  "video_id": "video-id",
  "upload_url": "https://rupload.facebook.com/video-upload/video-id"
}
```

## Step 2: Upload the Reel to rupload.facebook.com

**Send the file's bytes, or a public URL, to the upload host.** The token goes in an `Authorization: OAuth` header, not the query string:

```bash
curl -X POST "https://rupload.facebook.com/video-upload/v26.0/$VIDEO_ID" \
  -H "Authorization: OAuth $PAGE_TOKEN" \
  -H "offset: 0" \
  -H "file_size: $FILE_SIZE_BYTES" \
  --data-binary "@reel.mp4"
```

For a file you already host, replace the body with a `file_url` header:

```bash
curl -X POST "https://rupload.facebook.com/video-upload/v26.0/$VIDEO_ID" \
  -H "Authorization: OAuth $PAGE_TOKEN" \
  -H "file_url: https://cdn.example.com/reel.mp4"
```

Meta's rules for hosted files:

- **Public http or https**, with no authentication on the URL.
- **No Meta CDN URLs.** `fbcdn` links get rejected.
- **The host must allow** the `facebookexternalhit/1.1` user agent, and robots.txt can't block the file.

Both return `{"success": true}`.

## Step 3: Finish and publish the Reel

**POST to `video_reels` again with `upload_phase=finish` and a `video_state`.** This is where the caption goes:

```bash
curl -X POST "https://graph.facebook.com/v26.0/$PAGE_ID/video_reels" \
  -d "access_token=$PAGE_TOKEN" \
  -d "video_id=$VIDEO_ID" \
  -d "upload_phase=finish" \
  -d "video_state=PUBLISHED" \
  -d "description=Behind the scenes of launch day"
```

| Parameter | What it does |
|---|---|
| `video_state` | `PUBLISHED`, `DRAFT`, or `SCHEDULED` |
| `description` | The Reel's caption |
| `title` | Optional title |
| `scheduled_publish_time` | Unix time, 10 minutes to 29 days out, with `video_state=SCHEDULED` |
| `place` | A Page ID to tag as the location |
| `is_ai_generated` | Labels the Reel as AI-generated. Defaults to `false` |

**`{"success": true}` only confirms that Meta accepted the Reel.** It goes live after processing, so poll the status.

## Step 4: Poll the video status

**GET the video with `fields=status`** until processing and publishing both finish:

```bash
curl "https://graph.facebook.com/v26.0/$VIDEO_ID?fields=status&access_token=$PAGE_TOKEN"
```

The `status` object has one overall value and three phases:

| Field | Values | What to watch for |
|---|---|---|
| `video_status` | `uploading`, `upload_complete`, `processing`, `ready`, `error`, `upload_failed`, `expired` | `ready` means done; `error` or `upload_failed` means start over |
| `uploading_phase` | `status`, `bytes_transfered`, `errors` | `bytes_transfered` is your resume offset |
| `processing_phase` | `status`, `error` | Encoding problems surface here |
| `publishing_phase` | `publish_status` (`published`, `scheduled`, `draft`, `error`), `publish_time` | `published` means it's on the Page |

**Resuming an interrupted Reel upload:** read `uploading_phase.bytes_transfered` and POST to the same rupload URL again with that number as the `offset` header.

## What are the Facebook Reels video requirements?

**A vertical 9:16 MP4 of 3 to 90 seconds.** From Meta's [Reels Publishing guide](https://developers.facebook.com/docs/video-api/guides/reels-publishing):

| Spec | Requirement |
|---|---|
| File type | MP4 recommended |
| Video codec | H.264 or H.265; VP9 and AV1 also supported |
| Aspect ratio | 9:16 |
| Resolution | 1080x1920 recommended, 540x960 minimum |
| Frame rate | 24 to 60 fps, fixed |
| Duration | 3 to 90 seconds (60 seconds for a Reel shared as a Story) |
| Encoding | 4:2:0 chroma subsampling, closed GOP of 2 to 5 seconds, progressive scan |
| Audio | AAC Low Complexity, stereo, 48 kHz, 128 kbps or higher |

**Limits:** 30 API-published Reels per Page in a rolling 24 hours, and 10 collaborator invitations per Page per day. Meta also runs a copyright check on uploaded Reels, which you can read with `GET /{video-id}?fields=copyright_check_information`. It can take a couple of minutes.

## How do I upload a feed video to a Facebook Page?

**Upload the file with the Resumable Upload API, then publish the file handle to `/{page-id}/videos`.** Meta's [video publishing guide](https://developers.facebook.com/docs/video-api/guides/publishing) uses three calls:

1. **Start a session** on your app with the file's name, size in bytes, and type. Video must be `video/mp4`:

```bash
curl -X POST "https://graph.facebook.com/v26.0/$APP_ID/uploads?file_name=launch.mp4&file_length=$FILE_SIZE_BYTES&file_type=video/mp4&access_token=$USER_TOKEN"
```

   The response is `{"id": "upload:<UPLOAD_SESSION_ID>"}`.

2. **Upload the bytes** to the session. The token must be in the header, or Meta says the call fails:

```bash
curl -X POST "https://graph.facebook.com/v26.0/upload:$UPLOAD_SESSION_ID" \
  -H "Authorization: OAuth $USER_TOKEN" \
  -H "file_offset: 0" \
  --data-binary "@launch.mp4"
```

   The response is a file handle: `{"h": "2:c2FtcGxl..."}`.

3. **Publish the handle** to the Page with the Page token:

```bash
curl -X POST "https://graph-video.facebook.com/v26.0/$PAGE_ID/videos" \
  -F "access_token=$PAGE_TOKEN" \
  -F "title=Launch day" \
  -F "description=What we shipped this week" \
  -F "fbuploader_video_file_chunk=$FILE_HANDLE"
```

   The response is the video ID: `{"id": "<VIDEO_ID>"}`.

**To resume an interrupted feed upload,** `GET /upload:<UPLOAD_SESSION_ID>` with the same header. The response's `file_offset` tells you where to restart step 2.

Two things in Meta's docs to know before you build on this:

- **The host is in flux.** The publishing guide's step 3 uses `graph-video.facebook.com`, while Meta's [Video API overview](https://developers.facebook.com/docs/video-api/overview) says that host "has been deprecated" for video uploads. Test against `graph.facebook.com` too.
- **There's a shortcut for hosted files.** The [Page videos reference](https://developers.facebook.com/docs/graph-api/reference/page/videos/) accepts a `file_url` parameter, so Meta fetches the video itself. It also takes `published`, `scheduled_publish_time`, and a `thumb` image.

Feed videos are processed asynchronously too. Poll `GET /{video-id}?fields=status` as in Step 4.

## Is there a faster way to upload video to Facebook?

**Yes, if you'd rather not run two upload flows.** Everything above sits on top of a Meta app, App Review for `pages_manage_posts`, a token for every Page, and a polling loop for every video. Review is the slow part. When we went through it for Pages publishing, reviewers cared most about the screencast, so record the finished Reel playing on the Facebook Page itself.

[Adeli's Facebook API](https://www.tryadeli.com/product/facebook) takes that work off your team. Adeli maintains the Facebook developer app, goes through Meta's review, and refreshes every connected Page's tokens. You send the video to [`POST /api/v1/posts`](https://www.tryadeli.com/docs/api/posts/publish-facebook) and Adeli stages it on its own media host for Meta to fetch:

```bash
curl -X POST "https://app.tryadeli.com/api/v1/posts" \
  -H "Authorization: Bearer $ADELI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "platform": "facebook",
    "accountId": "00000000-0000-0000-0000-000000000000",
    "message": "Behind the scenes of launch day",
    "reel": { "contentType": "video/mp4", "base64": "<base64 MP4>" }
  }'
```

- **Reel or feed video.** Send `reel` for one vertical video published as a Reel, or `video` for a feed video, which also takes a `title` of up to 255 characters.
- **`202` with a `publishId`.** Video publishes asynchronously, so the response carries a `publishId` and a `statusUrl` instead of the finished post.
- **Size:** up to 64 MiB of MP4 decoded from base64 in a JSON request.

Then poll [Facebook video status](https://www.tryadeli.com/docs/api/posts/get-facebook-video-status) with the `publishId`:

```bash
curl "https://app.tryadeli.com/api/v1/posts/facebook/status?accountId=$ACCOUNT_ID&publishId=$PUBLISH_ID" \
  -H "Authorization: Bearer $ADELI_API_KEY"
```

Adeli folds Meta's four status fields into **one `status`: `PROCESSING`, `READY`, or `ERROR`**. A `READY` response includes the `permalink`. An `ERROR` includes `failureReason`, which is Meta's own explanation of what was wrong with the video, usually its length or aspect ratio.

The same [posting API](https://www.tryadeli.com/social-media-posting-api) also uploads video to [YouTube](https://www.tryadeli.com/product/youtube), TikTok, Instagram, X, and Bluesky. On [Adeli's pricing](https://www.tryadeli.com/pricing), **your first 3 connected accounts are free**, and one Facebook Page counts as one connected account.

## Frequently asked questions

### How many Reels can a Facebook Page publish through the API?

30 per Page in a rolling 24-hour period. Meta enforces the limit on the video_reels endpoint, so the 31st Reel fails until the window moves forward. Feed videos posted to the videos edge aren't counted against it.

### Can I upload a Facebook video from a URL instead of a file?

Yes. For Reels, send a file_url header to rupload.facebook.com instead of the file's bytes. The host must be public, allow the facebookexternalhit user agent, and not be a Meta CDN URL. For feed videos, the videos edge accepts a file_url parameter.

### Can I post Reels to a personal Facebook profile with the API?

No. Meta's Reels Publishing API only publishes to Facebook Pages. There's no API for posting Reels or videos to a personal profile or a Group.

### Can I schedule a Facebook Reel through the API?

Yes, on Meta's API. Finish the upload with video_state set to SCHEDULED and a scheduled_publish_time between 10 minutes and 29 days out. Feed videos accept scheduled_publish_time between 10 minutes and 6 months out.

## Related Adeli pages

- [Facebook API for Developers](https://www.tryadeli.com/product/facebook.md): Publish, schedule, and read analytics for Facebook Pages through one API. Adeli owns the Facebook developer app, token refresh, and Meta review. Free to start.
- [Social Media Posting & Scheduling API](https://www.tryadeli.com/social-media-posting-api.md): Adeli's social media posting and scheduling API publishes to Instagram, TikTok, YouTube, Facebook, X, and Bluesky in one request, now or at a set time. Free to start.
- [All posts on the Adeli blog](https://www.tryadeli.com/blog.md)

## About the author

Mika Reyes is the CEO behind Adeli and a tech and AI content creator. She was previously co-founder and CEO of Parallax, which was acquired by Phantom, and a product lead at LinkedIn. She is a Forbes 30 Under 30 honoree.
