# Facebook Reels Uploader (`dami_studio/facebook-reels-uploader`) Actor

Publish a Reel to your Facebook Page or profile from a URL. A Page token uses Meta's official Graph API (cheaper, no browser); cookies drive the composer, the only route to a profile. The publish fee needs a confirmed publish; metered bandwidth is billed per megabyte on every outcome.

- **URL**: https://apify.com/dami\_studio/facebook-reels-uploader.md
- **Developed by:** [Dami's Studio](https://apify.com/dami_studio) (community)
- **Categories:** Social media, For creators, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $350.00 / 1,000 reel published (browser)s

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Facebook Reels Uploader

Publish a Reel to a Facebook **Page** or to your **personal profile**, from a public video URL.

The Actor ships two completely different routes to Facebook and picks one from your input:

- **The Graph API lane.** You supply a Page access token. The Actor calls Meta's official Reels
  Publishing API: no browser, no proxy, real Meta error codes, and support for scheduling and
  drafts. This lane is faster, far more reliable and much cheaper. Use it whenever you can.
- **The browser lane.** You supply your own exported Facebook session cookies and the Actor drives
  the web composer. This is the **only** route that can reach a personal profile — and it carries a
  real account risk, described below in *Read this before you use cookies*.

Every run writes exactly one dataset row and finishes as **SUCCEEDED**, even when it fails: a failed
run costs you the same platform start fee as a successful one, so this Actor spends that fee telling
you precisely what went wrong instead of crashing.

***

### Read this before you use cookies

Driving facebook.com with injected cookies is **against Meta's Terms of Service and its
Automated-Activity policy.** The realistic failure mode is not a broken button. It is a
**checkpointed or disabled account** — and if you were publishing to a Page, the Page goes with it.
Meta's error 368 ("action deemed abusive or otherwise disallowed") is an integrity block, not a rate
limit: retrying makes it worse and lengthens it.

Three things follow from that, and they are recommendations, not decoration:

1. **Use the API lane for Pages.** It is the supported route, it needs no App Review (see below),
   and it puts nothing at risk.
2. **If you must use cookies, use a segregated account** — one you can afford to lose, not the
   account your business depends on.
3. **Space uploads out.** No more than roughly ten a day per account through the UI.

The browser lane exists because Meta offers no alternative for personal profiles. It does not exist
because it is a good idea.

***

### How the lane is chosen

| What you supply | Lane used | Why |
|---|---|---|
| `pageAccessToken` + `pageId` | **Graph API** | Supported, no browser, no proxy, real error codes, drafts and scheduling, and Meta fetches the video itself |
| Cookies + `target = profile` | **Browser** | The only route that exists for a personal profile |
| Cookies + `target = page`, no token | **Browser** | Your choice — the run logs a loud pointer at the API lane |
| Both | **Graph API** | Better on every axis |
| Neither | **Demo** | A labelled sample row. Nothing is downloaded, nothing is uploaded and nothing is charged |

**Reels can only be API-published to Pages — never to a personal profile.** Meta states it flatly:
*"You can only publish Reels to Facebook Pages."* The `publish_actions` permission that once allowed
profile posting was removed in Graph API v3.0 in 2018 and has not returned. There is no permission,
no review tier and no token that unlocks it. If you pass a token with `target = profile`, the Actor
tells you so up front rather than failing later for a reason that sounds like a bug.

***

### The API lane (recommended for Pages)

**You do not need App Review.** Meta's own documentation says: *"If your app will only be used by app
users who have a role on the app itself, App Review is not required."* You create the app, you are
its admin, and you admin the Page — so Standard Access is enough, Business Verification does not
apply, and no screencast has to be submitted to anyone. The setup is one-time and takes about ten
minutes.

1. Go to **developers.facebook.com** and log in with the account that admins the Page.
2. **My Apps → Create App**. Pick the **Business** app type. Name it anything.
3. In the app dashboard, add the **Facebook Login** product (you only need it to mint a token).
4. Open the **Graph API Explorer** (Tools → Graph API Explorer) and select your new app.
5. In **Permissions**, add: `pages_show_list`, `pages_read_engagement`, `pages_manage_posts`.
6. Click **Generate Access Token** and complete the dialog, choosing the Page you want to publish to.
   This gives you a short-lived **user** token.
7. Switch the token dropdown from *User Token* to **Page Token** and pick your Page. The token in the
   box is now a **Page** token — this is the one that matters. A user token with the same permissions
   is **not** enough.
8. Open the **Access Token Debugger** (Tools → Access Token Debugger), paste the token and click
   **Extend Access Token**. You now hold a long-lived Page token, which does not expire on a clock.
9. Confirm your role: `GET /me/accounts?fields=id,name,tasks` must list your Page with
   `CREATE_CONTENT` in its `tasks` array. A token from an ANALYZE- or ADVERTISE-only role
   authenticates perfectly and then fails at publish time with error code 200.
10. Paste the token into **Page access token** and the Page's numeric id into **Page ID**.

Long-lived Page tokens do not expire on a schedule, but they *are* killed by a password change, by
revoking the app's permissions, or by a Meta security invalidation. When that happens the run reports
`API_TOKEN_INVALID` and you regenerate it with the same steps.

On this lane the Actor hands Meta your `videoUrl` and Meta downloads the file itself, so nothing
large moves through the run — which is why this lane is priced so much lower.

**If Meta cannot fetch your URL, the run does not fail.** Meta fetches anonymously, so a signed link
that has expired, a host that blocks Meta's fetcher, or a URL with a username and password embedded
in it will all refuse that fetch. When that happens the Actor downloads the video itself and uploads
the bytes to Meta directly, resuming from Meta's own byte offset if the transfer is interrupted (up
to three attempts). It is slower and it is the only case where a file moves through this lane, so a
publicly fetchable `https` URL is still the path worth aiming for — but a URL Meta cannot reach is a
detour, not a dead end.

***

### The browser lane: exporting your cookies

Only needed if you are publishing to a **personal profile**, or if you would rather not create an
app. Read *Read this before you use cookies* first.

1. Open **facebook.com** in Chrome or Edge, signed in as the account that will publish.
2. Press **F12** to open DevTools, then go to **Application → Storage → Cookies →
   `https://www.facebook.com`**.
3. Find the row named **`c_user`** and copy its **Value** column. That is your numeric account id.
   Paste it into the **Facebook "c\_user" cookie** field.
4. Find the row named **`xs`** and copy its Value. It looks like `12%3Aabc...%3A2%3A1690...`. Paste
   it **exactly**, including every `%3A`, into the **Facebook "xs" cookie** field.
5. Find the row named **`datr`** and copy its Value into the **Facebook "datr" cookie** field. This
   one is optional but strongly recommended: `datr` is how Facebook recognises the browser that
   normally uses this account. A session arriving without it looks like a brand-new device and is
   much more likely to be met with a security checkpoint. Never invent a value — a `datr` Facebook
   has never seen is worse than none at all.

Prefer to hand over the whole jar? Export the entire `facebook.com` cookie list with a cookie
manager extension and paste the JSON into **Whole Facebook cookie jar**. Export it from a
**facebook.com** tab where you are signed in: messenger.com and instagram.com are separate cookie
jars and will not work.

Filling both is fine and nothing gets silently dropped. If you paste a raw `Cookie:` header, the two
are merged and the named fields win where they overlap. If you paste a JSON export that already
contains `c_user` and `xs`, the export is used whole — it is the richer jar — and the run says so in
the log rather than quietly ignoring what you typed.

Three more things worth knowing:

- **Set the proxy country** to the country where the cookies were created. A country jump is the
  single biggest checkpoint trigger this Actor sees. The browser's timezone and locale are set from
  that same country, so the environment and the exit tell Facebook one consistent story rather than
  two contradictory ones.
- Your cookies are **secret input fields**. They are never written to the dataset, never written to
  the key-value store, and never printed in the log — the log reports only a length, like `<42ch>`.
- Cookies are revoked by logging out, by "Log out of all sessions", and usually by a password
  change. When that happens the run reports `SESSION_INVALID` and you re-export.

***

### What gets checked before anything is transferred

Meta rejects a surprising number of technically-fine videos, and it does it *after* the upload. This
Actor checks the file first, so a doomed clip costs you the start fee instead of a full run:

| Check | Rule |
|---|---|
| Duration | 3 seconds minimum. On the API lane, 90 seconds maximum — Meta's 2025 "no length limit" announcement applies to the Facebook app, not to the API. On the browser lane, over 90 s is a warning and 40 minutes is the hard ceiling |
| Aspect ratio | 16:9 through 9:16 inclusive. Anything outside is refused; anything that is not vertical 9:16 gets a warning, because it will be pillar- or letterboxed in the Reels player |
| Resolution | 540x960 or larger |
| Frame rate | 24-60 fps, and **constant**. A variable frame rate is the single most common silent Reels rejection — screen recorders, phone slow-motion and AI generators all emit it. Fix: `ffmpeg -i in.mp4 -r 30 -vsync cfr -c:a aac -b:a 128k out.mp4` |
| Audio | AAC-LC, 128 kbps+, 48 kHz stereo is what Meta specifies. Anything else is a warning, not a refusal — Facebook will transcode it |
| File | 1 KB minimum and 2 GB maximum are Meta's own bounds, and it has to actually be a video. This Actor also applies its own ceiling to any file it has to move itself — see "Notes and limits" |
| Caption | 2,200 characters. Meta publishes no documented limit for Reels descriptions, so this is a deliberately conservative ceiling |

***

### What "confirmed" means here

A closed composer is **not** a published reel, and a `200` from Meta's finish call is not one either.
Transcoding and integrity review both happen asynchronously, and both can reject a video minutes
after the upload appears to have worked.

So the Actor confirms the publish before it reports success:

- **Browser lane** — it lists the reels on the account *before* publishing, then polls for up to
  three minutes afterwards for a `/reel/<id>` that was not there before. If none appears, the run
  reports `UPLOAD_UNCONFIRMED`, tells you to check the account before retrying so you do not
  double-post, and charges **no publish fee**. The video had already been pushed to Facebook by that
  point, so the bandwidth it used is billed by the megabyte; the row's `chargeNote` gives the figure.
- **API lane** — it polls Meta's own status endpoint until `publishing_phase.status` is `complete`.
  If Meta reports a processing error instead, or never reports the reel as published, the run reports
  `API_PROCESSING_FAILED` and charges nothing at all — the API lane has no metered connection and
  never bills per megabyte. Where Meta issued a video id for the upload, that id
  is in the message and in the row's `reelId`, so you can check the Page for it instead of retrying
  blind and posting the reel twice.

The caption is read back off the composer before publishing, and the "also share to Instagram" toggle
is read back too. If the caption cannot be verified, or the Instagram toggle's state cannot be read,
the Actor refuses to publish: a reel with the wrong caption, or a post to a platform you did not ask
for, is worse than no reel.

***

### What we cannot detect

**Music and rights takedowns produce no signal at all.** A reel can publish successfully, and then be
muted, geo-blocked or suppressed minutes later because its audio matched a rights holder's catalogue.
Meta emits no error, no webhook and no status change for this. The Actor reports the publish it
confirmed — it cannot promise the reel is still playable an hour later, and it does not pretend to.

Reach, distribution and whether Facebook shows your reel to anyone are likewise outside anything an
uploader can see.

***

### Pricing, and why there is a per-megabyte event

Three events. The publish fee is charged only for a publish that really happened; the bandwidth
event is charged for bandwidth that really moved, which is not the same thing:

| Event | Fires |
|---|---|
| `facebook-reel-upload` | Browser lane, once, **only** after a new reel id is confirmed on the account |
| `facebook-reel-upload-api` | API lane, once, **only** after Meta reports the reel as published |
| `video-megabyte` | Browser lane, **only when the run used this Actor's metered residential exit** — for the megabytes that run actually moved, on every outcome, including a run that published nothing |

**A run that publishes nothing is never charged a publish fee.** That has not changed and will not.

**A run that publishes nothing is still charged for the bandwidth it had already spent**, when that
bandwidth crossed this Actor's own metered residential connection. This is a change from how the
Actor behaved before 2026-08-25, and it is worth being plain about why. Reaching Facebook's composer
downloads your video and pulls tens of megabytes of Facebook's own interface across a residential
connection that is billed to this Actor by the gigabyte. Those bytes are bought the moment they move.
If your cookies turn out to be dead at the composer, or Facebook refuses the clip, or the upload is
never confirmed, the bytes are gone either way — and previously the run absorbed all of them and told
you it had charged you nothing. It now bills for what it moved, and says so on the row.

Four things bound that charge, and each of them is enforced in code, not promised in prose:

1. **Measured bytes only.** The count comes from bytes this run actually moved, counted as they
   moved. Not the file size you declared, not the size the host advertised, and never an estimate. A
   leg that could not be measured is billed as **zero**, not guessed at.
2. **Only bytes this Actor paid for.** Bytes that crossed a connection you supplied are never billed
   — see "When there is no metered connection" below. And the download from your own video host,
   which is ordinary transfer rather than residential bandwidth, is billed at **what it cost**, not
   at the per-megabyte rate: a 500 MB download that died mid-transfer recovers about $0.10, not $10.
   That download is requested uncompressed (`accept-encoding: identity`) precisely so the byte count
   is a wire count rather than a decompressed one; if your host sends a compressed body anyway, the
   leg is marked unmeasured and billed as zero. And a download too small to be worth a whole event
   rounds to nothing rather than up to one.
3. **Never more than success would have cost.** A failed run is never billed more than a confirmed
   publish of the same file would have been billed.
4. **Never past your own limit.** If you set `maxTotalChargeUsd`, the charge is clamped to what is
   left inside it, and the row and `CHARGE_SUMMARY.json` both say how many megabytes were dropped and
   absorbed by this Actor.

**And the run still refuses before spending, not after.** If your `maxTotalChargeUsd` cannot cover
the publish fee, the run stops before it contacts Meta, opens a proxy or downloads anything, and
charges nothing at all — charging on failure is about bytes that already moved, never a licence to
spend first and bill afterwards. The same check turns your limit into a file-size ceiling, so a file
too expensive for your budget is refused before a byte of it is pulled.

The Pricing tab carries the amounts that are actually billed; the current design prices are $0.35 for
`facebook-reel-upload`, $0.15 for `facebook-reel-upload-api` and $0.02 for `video-megabyte`.

**Why the API lane is priced far below the browser lane.** It genuinely costs a fraction as much to
run: no browser, no proxy bandwidth, and with the API's `file_url` upload Meta fetches your video
itself, so not one megabyte of it moves through this Actor.

**Why the browser lane charges per megabyte, honestly.** On that lane the composer session normally
runs through a residential connection — a datacenter address on a write action is what gets accounts
checkpointed — and your video crosses that same metered connection on its way to Facebook. That
bandwidth is billed to this Actor by the gigabyte and it scales directly with your file. At the
design prices, the flat fee alone stops covering the cost of a run at roughly a **20 MB** video, and
a 90-second 1080x1920 H.264 reel is normally **20-100 MB**. In other words the median customer file
sits exactly at that break-even point. The per-megabyte event is what keeps the flat fee from having
to be much higher for everybody, and it is why a large file is *expensive* here rather than refused.

**When there is no metered connection, there is no per-megabyte charge — on success or on failure.**
The event exists to recover one specific cost, so it is charged only when that cost is really
incurred. You pay the publish fee and nothing per megabyte whenever the run did not use this Actor's
residential exit — that is, when you supplied your own servers in `proxyUrls`, when you asked for a
datacenter-only proxy group, or when you switched the Apify proxy off. A **failed** run on any of
those exits is charged nothing at all. The run says so in the log, and the result row reports
`videoMegabytes: 0` with `bandwidth.metered: false`. The API lane never charges it either, for the
same reason: the bytes do not cross a metered connection there, so billing you for them would be
billing you for a cost that was never incurred.

**What is not covered, stated rather than glossed over.** If you abort a run, this Actor tries to
write its row and settle the bandwidth inside the few seconds the platform allows before it kills the
container — that is best effort and it can be cut short. A platform migration and the platform's own
hard run timeout get no such attempt at all: in those cases the bandwidth is absorbed by this Actor,
and no charge and no row are made.

***

### Output

One row per run. On success:

```json
{
  "ok": true,
  "lane": "api",
  "target": "page",
  "reelId": "1234567890123456",
  "reelUrl": "https://www.facebook.com/reel/1234567890123456",
  "caption": "Behind the scenes #reels",
  "videoMegabytes": 0,
  "confirmed": true,
  "chargeStatus": "pending",
  "chargeSummaryKey": "CHARGE_SUMMARY.json",
  "publishedAt": "2026-08-23T10:12:44.109Z",
  "warnings": []
}
```

Two fields are worth reading carefully:

- **`videoMegabytes` is the megabyte count this Actor asked to bill, not the size of your file** — and
  not, on its own, proof of what you were billed. On a successful run it is the file's megabytes; on
  a failed one it is the megabytes of bandwidth that had already moved. It is `0` whenever no
  per-megabyte event applied.
  The file's own size is in `videoSizeMegabytes`; the count the platform actually charged is
  `megabytesCharged` in the settled record described next.
- **`chargeStatus` is `pending` in the row itself, and stays that way.** The row is written *before*
  the charge is made, because this Actor charges only after a result has actually been delivered to
  you — so the row cannot know what the platform answered, and an Apify dataset row cannot be edited
  afterwards. Rather than leave you guessing, the row names where the answer landed:
  **`CHARGE_SUMMARY.json`** in the run's key-value store, which carries `chargeStatus: "settled"`,
  the reel's id and URL, and the counts the platform actually charged. The same figures are printed
  in the log as a `Billed:` line and set as the run's status message. If an event has no price, or
  your own maximum-charge limit was reached, that is where you will see it — the run says "you were
  NOT billed" rather than quietly claiming otherwise.

On failure, one diagnostic row with `ok: false`, an `errorCode` and a sentence you can act on. It
carries no publish fee, and it carries a `bandwidth` block plus a `chargeNote` stating in words what
is being billed for the megabytes the run had already moved and why — with the settled figure, as
always, in `CHARGE_SUMMARY.json`:

```json
{
  "ok": false,
  "errorCode": "UPLOAD_REJECTED",
  "charged": null,
  "chargeStatus": "pending",
  "videoMegabytes": 32,
  "bandwidth": {
    "exit": "residential",
    "metered": true,
    "inboundBytes": 20971520,
    "inboundMeasured": true,
    "inboundEncoding": null,
    "meteredWireBytes": 12582912,
    "meteredWireMeasured": true,
    "meteredOutBytes": 20971520,
    "meteredOutMeasured": true,
    "megabytesBillable": 32,
    "megabytesRequested": 32,
    "capTruncated": false,
    "cappedAtSuccessCost": false
  }
}
```

Any `...Measured: false` in that block means that leg could not be measured on this run, and an
unmeasured leg is billed as **zero** — never estimated from the file size or from a declared length.
`inboundMeasured: false` has one specific cause: your video host answered with a compressed
`content-encoding` even though this run asked it for the file uncompressed, which means the only byte
count available here is the decompressed one and the number of bytes that really crossed the wire is
not knowable. `inboundEncoding` names the coding the host used. The browser lane also saves a screenshot and the page HTML to this run's
key-value store, named after the error code, so a Facebook layout change can be diagnosed without
repeating the run.

| Error code | What it means |
|---|---|
| `BAD_INPUT` | Something in the input is wrong; the message names the field and the fix |
| `OVERSIZED_VIDEO` | Above this Actor's file ceiling. The ceiling is set by the Actor's owner and no input raises it — the video is downloaded straight from its host into this Actor before any proxy is involved, so the cost it caps is the same whichever exit the run uses. On the API lane, hosting the file where Meta can fetch it anonymously removes the limit entirely, because then none of it moves through this Actor |
| `VIDEO_UNAVAILABLE` | The `videoUrl` did not deliver a file (HTTP error, timeout, expired signed link) |
| `SESSION_INVALID` | The cookies are not a logged-in session. Re-export them |
| `SESSION_CHECKPOINT` | Facebook is showing a security checkpoint. Clear it by hand, then re-export |
| `SESSION_2FA_REQUIRED` | The account wants a 2-factor code, which no cookie can supply. Supply the real `datr` |
| `ACTION_BLOCKED` | An integrity/abuse block, not a rate limit. Do not retry — wait at least 24h |
| `COMPOSER_UNAVAILABLE` | Desktop reel creation is not available on this personal profile; Facebook gates it per account. Use a Page, or post from the mobile app |
| `LOCALE_UNSUPPORTED` | The Facebook interface is not in English and a step needed a text label |
| `LAYOUT_CHANGED` | A read-back failed — Facebook changed the page. The failing locator is named in the message |
| `CROSSPOST_UNCONFIRMED` | The Instagram toggle could not be verified, so nothing was published |
| `UPLOAD_STALLED` | Facebook never finished processing the video |
| `UPLOAD_REJECTED` | Facebook refused the clip, usually a duration or format problem |
| `UPLOAD_UNCONFIRMED` | The composer closed but no new reel appeared. Check the account before retrying |
| `TIMED_OUT` | The run hit its time budget before Facebook confirmed anything |
| `ABORTED` | You aborted the run. Written on a best-effort basis inside the seconds the platform allows before it kills the container, so it may not appear. Check the account before re-running: an abort can land after the composer has already posted |
| `NETWORK` | The proxy or DNS failed |
| `API_TOKEN_INVALID` | Graph error 190 — regenerate the Page token |
| `API_PERMISSION` | The token lacks `CREATE_CONTENT` on that Page, or is a user token rather than a Page token |
| `API_RATE_LIMIT` | Meta throttled the Page. Note that Meta allows 30 API-published reels per Page per rolling 24 hours, and scheduled reels count against the same budget |
| `API_ABUSE_BLOCK` | Graph error 368. Hard stop, and retrying looks more abusive |
| `API_SPEC_REJECT` | Meta rejected the video's format. Permanent — re-encode, do not retry |
| `API_UPLOAD_FAILED` | The transfer to Meta failed after three resume attempts |
| `API_PROCESSING_FAILED` | Meta accepted the upload and then failed to process it, or never reported it as published. The reel is NOT live, and nothing is charged — the API lane bills no publish fee here and never bills per megabyte. When Meta gave the upload a video id, the message and the `reelId` field carry it — check the Page for that reel before you retry, because a second run would upload it again |

***

### Notes and limits

- **Memory is fixed at 2048 MB** so the platform's per-gigabyte start fee cannot change under you.
- **The video is downloaded straight from its origin**, never through the proxy — proxying a file
  download costs many times more and buys nothing.
- **One sticky proxy exit is used for the whole run.** Rotating mid-session is itself a checkpoint
  trigger.
- **Bring your own proxy servers if you prefer.** Put them in the proxy setting's `proxyUrls`, or set
  the top-level `proxyUrls` field — both are honoured, and your servers are used verbatim, in the
  order you give them. What that saves you is precise: the **outbound** leg no longer crosses this
  Actor's metered residential connection, so the per-megabyte charge does not apply to that run — on
  a confirmed publish or on a failure. It
  does not raise the file size limit — it **lowers** it, from 500 MB to **304 MB**. The inbound leg is
  unchanged either way (the video is pulled straight from its host into this Actor before any proxy is
  involved), but an unmetered run earns only the flat fee, with no per-megabyte line to help carry a
  large file, so a smaller file is all that fee can cover. Selecting a non-residential proxy group, or
  switching the proxy off entirely, has the same effect for the same reason.
- **The file ceiling is set by this Actor's owner**, not by an input field — an input that could
  raise it would let anyone raise the owner's bandwidth bill without limit. On the browser lane it is
  **500 MB** on the default metered residential exit and **304 MB** on any unmetered one (your own
  `proxyUrls`, a non-residential group, or the proxy switched off). On the API lane, `file_url`
  uploads are bounded only by Meta's own limits (1 KB to 2 GB), because Meta fetches the file and none
  of it passes through this Actor; if Meta cannot fetch your URL and the bytes have to be pushed from
  this run instead, a smaller ceiling applies to that fallback — currently **107 MB**. None of these
  numbers is typed in by hand: each is derived from what that lane's fee can actually pay for, and the
  `OVERSIZED_VIDEO` message always names the exact figure that applied to your run. Hosting the video
  where Meta can download it anonymously removes the limit entirely.
- **Desktop reel creation on personal profiles is gated by Facebook per account.** If the composer
  does not render for yours, the Actor reports `COMPOSER_UNAVAILABLE` rather than clicking blindly.
  Publish to a Page, or post that clip from the mobile app.
- This Actor publishes; it does not download, scrape or repost anyone else's content. Publish only
  video you have the rights to.

# Actor input Schema

## `videoUrl` (type: `string`):

Public direct https:// URL to the .mp4 (S3, a CDN, an Apify key-value store — anything that serves the file itself rather than an HTML landing page). Required for a real run; leave the credential fields blank to get a free labelled sample row instead. The clip is checked before anything is transferred: 3 seconds or longer, 540x960 or larger, 24-60 fps, and inside Meta's 16:9-to-9:16 band. Vertical 1080x1920 H.264 + AAC is the format Meta documents.

## `caption` (type: `string`):

Reel description; put #hashtags here. Capped at 2,200 characters — Meta publishes no documented Reels description limit, so this Actor uses a conservative ceiling rather than risk a silent truncation it cannot verify. On the browser lane the caption is typed into the composer and then read back before publishing: if it does not match, the run stops instead of publishing a reel with the wrong text.

## `reelTitle` (type: `string`):

Optional title (Business Suite "Add a title" / API `title`). Capped at 255 characters. Separate from the caption, and not shown on every surface.

## `target` (type: `string`):

Selects composer route. A personal profile can ONLY be reached through the browser lane (cookies): Meta states "You can only publish Reels to Facebook Pages", so no access token of any tier can publish to a profile. A Page can be reached either way, and the API lane is the better one.

## `pageId` (type: `string`):

Numeric Page id. Required when target=page (and whenever a Page access token is supplied). Find it in Meta Business Suite under the Page's settings, or from /me/accounts in the Graph API Explorer. Not the @username.

## `shareMode` (type: `string`):

`schedule` / `draft` are Graph API-lane only in v1: they need a Page access token. On the browser lane anything other than "now" is rejected up front rather than silently published immediately.

## `scheduledPublishTime` (type: `string`):

Required when shareMode=schedule, e.g. 2026-09-01T14:30:00Z. Meta's own window applies: more than 10 minutes in the future and within 29 days. Scheduled reels count against the same publishing budget as immediate ones.

## `crosspostToInstagram` (type: `boolean`):

Browser lane only. Asserted by read-back; never left to the composer's default. If the toggle exists but its state cannot be read, the run refuses to publish rather than post to a second platform you did not ask for. The Graph API has no equivalent, so on the API lane this is ignored and the run says so in the log.

## `c_user` (type: `string`):

Browser lane. DevTools -> Application -> Cookies -> https://www.facebook.com, row `c_user`, copy the Value column. It is your numeric account id.

## `xs` (type: `string`):

Browser lane, same list. This is the session secret — it looks like 12%3Aabc...%3A2%3A1690... . Paste the value exactly, including the %3A sequences. Anyone holding this value holds your session, so treat it like a password.

## `datr` (type: `string`):

Browser lane, same list. Optional but strongly recommended: it identifies your browser to Facebook's security system, and a session arriving without the `datr` it normally carries reads as a brand-new device and often gets a checkpoint. Never invent one — a `datr` Facebook has never seen is worse than none.

## `cookies` (type: `string`):

Advanced alternative. Paste a Cookie-Editor JSON export of the whole facebook.com jar, or a raw `Cookie:` header. Leave blank if you filled the three fields above. Export from a facebook.com tab where you are signed in — messenger.com and instagram.com are separate cookie jars and will not work.

## `pageAccessToken` (type: `string`):

Graph API lane. A long-lived PAGE access token (not a user token) from a user with the CREATE\_CONTENT task on the Page. Supplying this switches the Actor to the official API and skips the browser entirely — cheaper and far more reliable. It needs no App Review, because you are the admin of your own Meta app; the README has the ten-minute setup. Requires `pageId`, and cannot publish to a personal profile.

## `graphApiVersion` (type: `string`):

Pinned. Never call unversioned: an unversioned Graph call silently follows Meta's newest release and changes behaviour under you. Only change this if Meta deprecates the pinned version.

## `continueOnNonEnglishUi` (type: `boolean`):

Browser lane. If your Facebook interface is not English, structural locators are used and text-anchored steps are best-effort. Off = the run stops with an actionable message instead of guessing which button it is about to click.

## `proxyConfiguration` (type: `object`):

Proxy for the browser lane (the API lane connects directly and needs no proxy). The default is a residential exit, because a datacenter address plus an unfamiliar device fingerprint on a write action is what produces a Facebook checkpoint — and that residential bandwidth is what the per-megabyte charge pays for. Set the COUNTRY to match where your cookies were exported: a country jump is the single biggest checkpoint trigger. Two choices take the run OFF the metered exit, and with it the per-megabyte charge on every outcome, a confirmed publish and a failure alike: selecting a non-residential GROUP, or putting your own servers in "Proxy URLs" (used exactly as given). Both raise the checkpoint risk, and both LOWER the file size limit from 500 MB to 304 MB — an unmetered run earns only the flat fee, so it can afford a smaller file, not a larger one. Switching the Apify proxy off entirely connects direct, with the same trade-off.

## `proxyUrls` (type: `array`):

Optional. Full proxy URLs (http://, https://, socks5://), used verbatim and in the order given, instead of this Actor's residential exit. Exactly equivalent to "Proxy URLs" inside the Proxy setting above — this top-level spelling exists because an API caller can set it without building a whole proxy-configuration object. Using your own servers removes the per-megabyte charge for that run — whether it publishes or fails — because the outbound bytes no longer cross this Actor's metered connection. It does not change the video size limit: the file is downloaded direct from its host into this Actor before any proxy is involved.

## `demoMode` (type: `boolean`):

Force a labelled SAMPLE row that uploads nothing and charges nothing, even if credentials are provided. A run with no credentials at all is automatically demo mode.

## Actor input object example

```json
{
  "caption": "",
  "reelTitle": "",
  "target": "profile",
  "shareMode": "now",
  "crosspostToInstagram": false,
  "graphApiVersion": "v25.0",
  "continueOnNonEnglishUi": false,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "demoMode": false
}
```

# Actor output Schema

## `results` (type: `string`):

One row per run: which lane published the reel, its id and URL, the caption, the billed video megabytes, and the publish timestamp. A run that did not publish writes one labelled diagnostic row instead, carrying the error code, a sentence explaining exactly what stopped it, and a bandwidth block stating which megabytes it had already moved and is billed for.

## `files` (type: `string`):

Debug artifacts from a failed browser-lane step — a screenshot and the page HTML, named after the error code — so a Facebook layout change can be diagnosed after the run without repeating it.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {};

// Run the Actor and wait for it to finish
const run = await client.actor("dami_studio/facebook-reels-uploader").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {}

# Run the Actor and wait for it to finish
run = client.actor("dami_studio/facebook-reels-uploader").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{}' |
apify call dami_studio/facebook-reels-uploader --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,dami_studio/facebook-reels-uploader"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/c0mOdQtnhvRmJUX86/builds/lyZzY1xogPCVdWeNh/openapi.json
