# Instagram Reel Uploader Pro - Auto-Post Reels (`dami_studio/instagram-reel-uploader-pro`) Actor

Post a Reel to Instagram from any public video URL: caption, published confirmation and a permalink back. The upload fee needs a confirmed post; the bytes a run really moves are billed per megabyte on any outcome, a failure included. Your own proxy, or no proxy, is never billed per MB.

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

## Pricing

from $400.00 / 1,000 reel uploadeds

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

## Instagram Reel Uploader

Posts a Reel to your own Instagram account from a URL. You give it the video, a caption, and your own exported session cookies, and it drives a headless browser through Instagram's web composer to publish the Reel. It's built to be the publish step of a faceless-content or scheduling pipeline.

### How it works

It downloads your video, launches a headless Chromium routed through your proxy, loads your exported Instagram cookies into the session, opens the Create composer on instagram.com, sets the file, advances through the crop and edit steps, writes the caption, and shares. There is no shared login. You bring your own session each run, exported from a browser where you're already signed in.

### Try it with no login (demo / sample mode)

Run it with **no cookies** (or set `demoMode: true`) and it returns one clearly-labeled **sample row** and uploads nothing — a try-before-you-export preview. The sample row carries `_demo: true` and a `_notice` explaining that no credentials were supplied. To post for real, supply your own session (below).

### Input

| Field | Required | Notes |
|---|---|---|
| `videoUrl` | for real posts | Public **https** direct URL to the `.mp4` to post as a Reel. You host it. Max 600 MB. |
| `cookies` | for real posts | **Your own** Instagram session, exported as Cookie-Editor JSON from a browser where you're logged in. Must include `sessionid` and `ds_user_id`. Stored as a secret. |
| `caption` | no | The Reel caption. Put your `#hashtags` here. |
| `proxyConfiguration` | no | On the default, the Actor opens its own metered residential connection, because Instagram checkpoints datacenter addresses — and that connection is what the per-megabyte charge pays for, on every outcome including a failure (see [Pricing](#pricing)). The proxy group you pick is ignored. Two things do take effect and both switch the per-megabyte charge off entirely, failed runs included: your own servers in `proxyUrls` are used verbatim, and unticking "Use Apify Proxy" connects directly. Instagram is much more likely to checkpoint a direct connection, so that saving is a trade-off, not free. |
| `demoMode` | no | Return a labeled sample row without uploading. Auto-enabled when no `cookies` are given. |

#### How to export your Instagram cookies

1. Log in to **instagram.com** in Chrome/Edge.
2. Install the **Cookie-Editor** extension and open it on the Instagram tab.
3. Click **Export → Export as JSON** (this copies a cookies array including `sessionid` and `ds_user_id`).
4. Paste that JSON into the `cookies` field. It is automating **your own** logged-in session — nothing is shared.

### Output

Exactly one dataset record per run, always.

- **Demo run:** `{ _demo: true, _sample: true, _notice, ok: true, demo: true, charged: false, reelUrl: ".../reel/SAMPLE/", ... }` — nothing uploaded.
- **Real success:** `{ ok: true, charged: null, chargeStatus: "pending", chargeSummaryKey, platform, caption, posted: true, confirmed: true, reelUrl, finalUrl }` — `confirmed` is only `true` after Instagram's "has been shared" confirmation is actually seen, and `reelUrl` is the real permalink when it can be read. The row has to be written *before* the charge is made (the charge only fires once the row is safely on the dataset), so it cannot state what you were billed: `charged` is `null` and `chargeStatus` is `"pending"`. The settled figures land in **`CHARGE_SUMMARY.json`** in the run's key-value store, and in the run's status message.
- **Anything else:** `{ ok: false, charged, chargeStatus, chargeNote, bytesMoved, errorCode, error, reason, hint }` — a diagnostic row naming what went wrong and what to do about it, plus a debug **screenshot + page HTML** saved in the run's key-value store and named in the row. `bytesMoved` is the measured bandwidth ledger for that run — what came in, what crossed the Actor's own connection, and how many megabytes of it are billable — and `chargeNote` says in plain English what you are being charged and why.

**A run that cannot post does not fail — it returns the diagnostic row and ends as SUCCEEDED.** A failed run and a succeeded run cost you the same $0.03 platform start fee, so crashing would charge you the same and tell you less. The `instagram-upload` flat fee fires only on the confirmed-success path. The `video-megabyte` bandwidth charge is different — see [Pricing](#pricing) — it is charged for the megabytes a run actually moved, on any outcome.

#### Error codes

| `errorCode` | What happened | What to do |
|---|---|---|
| `BAD_INPUT` | `videoUrl` or `cookies` was missing or unusable. | Fix the field named in `error`. |
| `VIDEO_UNAVAILABLE` | The `videoUrl` did not download. | Open it in a private window; it must return the `.mp4` directly. |
| `SESSION_INVALID` | The cookies are expired or were exported logged out. | Re-export with Cookie-Editor while signed in. |
| `SESSION_CHECKPOINT` | Instagram put the account behind a security check. | Clear it in a normal browser, then re-export the cookies. |
| `UPLOAD_REJECTED` | Instagram refused the post (action block, rate limit, file rejected). | Wait a few hours, post less often, use a residential proxy in your country. |
| `UPLOAD_UNCONFIRMED` | Share was clicked but no confirmation appeared. | Check the account before retrying — a blind retry can double-post. |
| `LAYOUT_CHANGED` | Instagram's composer no longer matches the steps this Actor drives. | Nothing on your side; report the run so the selectors can be updated. |
| `NETWORK` | A network or proxy error stopped the run reaching Instagram. | Retry, or switch proxy country. |
| `TIMED_OUT` | The run hit its time budget and stopped itself. | Check the account, then retry with a longer timeout. |
| `ABORTED` | You aborted the run, or the platform migrated it, before Instagram confirmed anything. | Nothing to fix. Check the account first in case Share had already been clicked. |
| `INTERNAL` | Unexpected error. | Report the run id with the saved screenshot. |

### Method & reliability

This is a **best-effort browser-automation** method: it drives Instagram's real web "Create" composer (there is no official Reels upload API for personal accounts). It checks your session is logged in **before** downloading your video, selects Reel where offered, advances through the crop/cover/edit steps with explicit waits and several fallback selectors per step, verifies the file registered and the caption took, then watches for either Instagram's "has been shared" confirmation or an explicit refusal for up to 3 minutes.

Because it depends on Instagram's live UI, a layout change on their side can break a step. When that happens you get a `LAYOUT_CHANGED` row and a screenshot, not a silent wrong result — and no publish fee, though the bandwidth the run had already spent getting there is billed by the megabyte on the Actor's own connection.

### Example

```json
{
  "videoUrl": "https://cdn.example.com/reel.mp4",
  "caption": "behind the scenes #reels",
  "cookies": "[{\"name\":\"sessionid\",\"value\":\"...\",\"domain\":\".instagram.com\",\"path\":\"/\"}]"
}
```

### Pricing

Three charges, no volume tiers and no plan gates:

| Charge | Amount | When it applies |
|---|---|---|
| Run start | $0.015 per event | Every run. The platform bills one start event per gigabyte of run memory, and this Actor is fixed at 2 GB, so a run start costs **$0.03**. |
| `instagram-upload` | $0.40 | Once per run, and **only** after Instagram's own "has been shared" confirmation. A run that does not publish never pays this. |
| `video-megabyte` | $0.02 per MB | The bandwidth charge. Only on runs that used this Actor's own metered residential connection, and charged for the megabytes that run **actually moved** — whether or not the Reel went live. |

#### The bandwidth charge, in plain words

The upload fee and the bandwidth charge answer two different questions, and only one of them depends on whether your Reel got posted.

**The $0.40 `instagram-upload` fee is charged only when Instagram confirms the Reel is live.** If the run does not publish, you are not charged it. That has not changed.

**The $0.02/MB `video-megabyte` charge is for bandwidth this Actor bought and spent on your run, so it is charged for the megabytes that actually moved, on any outcome — including a failure.** A run that downloads your file and drives Instagram's composer spends real, metered, per-gigabyte bandwidth long before it finds out whether the post will land. When Instagram answers with a checkpoint, an action block, or silence, that bandwidth has already been paid for. It is billed to you rather than absorbed.

What is counted is measured, never assumed:

- the megabytes of your video **that actually arrived** — a transfer cut short at 4 MB is billed 4 MB, not the file's declared size;
- the megabytes measured crossing this Actor's own connection while driving the composer.

What is **not** counted:

- anything on a run that used **your** proxy, or no proxy — see below, this is unchanged;
- any leg the Actor could not measure. If the browser's byte meter cannot attach, that traffic is billed as zero rather than estimated. The outbound push of your file to Instagram is only partly visible to the Actor and is never billed, only reported;
- a run that stopped **before moving anything** — bad input, a budget refusal, dead cookies caught by the pre-check, or a file refused on a size header the host gave up front. Those genuinely move nothing and genuinely cost you nothing but the start fee. The one exception, stated plainly because it is not free: if your host will not answer a size request at all and only declares the size once the download starts, the run has already opened its connection and checked your session by then, and those megabytes are billed. The result row says exactly how many.

Every run's row carries a `bytesMoved` block with the measured figures, and `CHARGE_SUMMARY.json` in the run's key-value store carries the settled charge the platform actually raised.

#### What a run costs

| Run | Over the Actor's residential connection | Over your own proxy, or with the proxy off |
|---|---|---|
| 30 MB Reel, published | $0.03 + $0.40 + $0.60 = **$1.03** | $0.03 + $0.40 = **$0.43** |
| 300 MB Reel, published | $0.03 + $0.40 + $6.00 = **$6.43** | $0.03 + $0.40 = **$0.43** |
| 30 MB Reel, composer refused it after the file was pushed | $0.03 + ~$1.00 of measured megabytes, **no $0.40 fee** | **$0.03** |
| Dead cookies, caught before anything was downloaded | **$0.03** | **$0.03** |
| Bad input, budget refusal, or a demo/sample run | **$0.03** | **$0.03** |

A failed run on the metered connection therefore lands in roughly the same place as a successful one: you do not pay the $0.40 publish fee, and the composer's own traffic is billed by the megabyte instead of by that fee. What you never pay twice for is the file — a failed run bills the megabytes that moved on exactly the same basis a successful one does.

#### When the per-megabyte line is zero

`video-megabyte` pays for a metered connection, not for the file, so it is not charged when the bytes did not go over that connection. **This is unchanged, and it applies to failed runs exactly as it applies to successful ones.** It is the case whenever you:

- put your own servers in `proxyConfiguration.proxyUrls` — you already paid for that bandwidth once; or
- untick "Use Apify Proxy", which connects directly from the run container.

Then the run pays the $0.03 start fee, plus the $0.40 upload fee if the Reel published, and nothing more, whatever the Reel weighs and however the run ends. Picking a different proxy *group* does not do this — the group is ignored and the run still uses the Actor's residential connection. The settled `CHARGE_SUMMARY.json` names the exit that was used and states plainly when no per-megabyte charge was made. The trade-off is real: Instagram treats an unfamiliar datacenter or container address logging into your account as suspicious, so those runs draw checkpoints and action blocks more often.

#### Why the per-megabyte charge exists

A Reel upload needs a residential connection — Instagram checkpoints the cheap ones — and residential bandwidth is billed by the gigabyte. Part of that traffic is fixed: measured on a real confirmed upload, a 2.8 MB Reel moved 23.2 MB across the connection, because about 20 MB of that is the web composer itself and is the same whatever you post. On a published Reel the flat $0.40 covers that fixed part and the video's own megabytes are billed on top, which is what lets a large file go through and simply cost more instead of being refused. On a run that does not publish there is no flat fee to cover it, so the same traffic is recovered by the megabyte instead.

#### Your `maxTotalChargeUsd` is still the ceiling

Set `maxTotalChargeUsd` on the run and it is honoured on every path, failures included. The Actor charges up to your limit and stops — it does not go past it and then bill you for the overshoot. When your limit stops the charge short of the megabytes that moved, the remainder is absorbed by this Actor and `CHARGE_SUMMARY.json` says so explicitly, naming both numbers.

The Actor also still refuses **before** spending. If your limit cannot cover the flat upload fee, or cannot cover the megabytes your file would move, the run stops with a `BAD_INPUT` row before it opens a connection, downloads anything or starts a browser — so a low limit can never turn into "we spent it anyway and billed you after". Raise `maxTotalChargeUsd` (or post a smaller file) and re-run. The file itself may be up to **600 MB** on any connection.

#### The one case this cannot cover

If you abort a run, or the platform migrates it, the Actor is given a short window to settle and it uses it — but a container that is killed outright writes nothing and charges nothing. That is not a promise that aborted runs are free; it is a statement that the Actor cannot reliably act after it has been stopped.

### FAQ

**Is there an official Instagram Reels upload API?**
No. Meta's Content Publishing API only covers Instagram Business and Creator accounts connected to a Facebook Page, and it needs an app review. This Actor drives the normal instagram.com web composer with your own logged-in session instead, so it works with a personal account.

**Do you get my Instagram password?**
No. You paste session cookies, not credentials, and the field is stored as a secret. Log out of that Instagram session to revoke them.

**Why did my run come back with `SESSION_INVALID`?**
Instagram session cookies expire, and they are also invalidated when you log out or change your password. Re-export them with Cookie-Editor from a tab where you are signed in.

**Will I be charged if the Reel does not post?**
You are never charged the $0.40 `instagram-upload` fee unless Instagram confirms the Reel is live. You *are* charged $0.02/MB for the bandwidth the run actually moved on this Actor's own residential connection, because that bandwidth is bought and spent before Instagram gives its answer — so a run that downloads your file and reaches the composer costs money whether or not the post lands. A run that stops before moving anything (bad input, a budget refusal, cookies caught by the pre-check) costs the $0.03 start fee alone, and a run on your own proxy is never charged per megabyte at all. Every row carries a `bytesMoved` block and every run writes a `CHARGE_SUMMARY.json` with the settled figure.

**Can it post to a Story, a carousel, or a feed photo?**
No. Reels only, one video per run.

**Why does it need a proxy?**
Instagram treats a datacenter address logging into your account as suspicious, so the Actor opens a residential connection by default. That connection is metered by the gigabyte, which is what the $0.02/MB `video-megabyte` charge pays for — on successful and failed runs alike, because the gigabytes are bought either way. Choosing a different proxy group in the input has no effect; supplying your own servers in `proxyUrls`, or turning the proxy off entirely, does — and on either of those the per-megabyte charge is not made at all, whatever the outcome.

### What this does not do

- It does not create or find your video. You host the `.mp4` and pass a public https URL.
- It does not schedule. One run posts one Reel, immediately.
- It does not manage multiple accounts in one run. One session, one Reel.
- It cannot clear a checkpoint or an action block for you. Those need a real browser on the account.
- It does not guarantee delivery when Instagram changes its composer. That is reported as `LAYOUT_CHANGED` with a screenshot, never as a silent success.

### Notes

This automates your logged-in Instagram session, which Meta's terms discourage, so run it on your own account at your own discretion. Automating a session from a new device/IP is a pattern Instagram may flag with a temporary action-block or checkpoint — a residential proxy in your own country reduces (but does not eliminate) this. Cookies expire, so re-export them when you see a "not logged in" error. Because it drives Instagram's actual web UI, a layout change on their side can break a selector and need an update — failures are surfaced clearly with a saved screenshot, never as a silent success.

# Actor input Schema

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

Public direct https URL to the video (.mp4) to upload as a Reel. Required for a real post; leave everything empty to get one free labelled sample row instead, which uploads nothing, moves no bandwidth and is charged nothing.

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

The Reel caption (include #hashtags here).

## `sessionid` (type: `string`):

Your Instagram "sessionid" cookie. In your browser: DevTools > Application > Cookies > https://www.instagram.com, find the row named sessionid and copy its Value column. Paste just the value.

## `ds_user_id` (type: `string`):

Your Instagram "ds\_user\_id" cookie, from the same Cookies list. It is the numeric account id, and it is also the run of digits at the very start of sessionid.

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

Optional. Only needed if you would rather hand over the whole jar than the two fields above: paste a Cookie-Editor JSON export, or a raw "Cookie:" header. Leave blank if you filled in sessionid and ds\_user\_id.

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

Leave this on the default. Instagram answers datacenter addresses with a security checkpoint, so the default is a residential connection - and that bandwidth is what the per-megabyte charge pays for. Because the Actor buys those gigabytes before Instagram says whether your post will land, the per-megabyte charge applies to the megabytes a run actually moved on this connection even when the post fails. The flat upload fee is never charged unless the Reel is confirmed published. The proxy GROUP you pick here is not used. Two things here DO take effect, and both remove the per-megabyte charge entirely - failed runs included - because the video stops crossing a connection this Actor pays for: your own servers in "proxyUrls" are used exactly as given, and unticking "Use Apify Proxy" connects directly with no proxy at all. Both raise the risk of a checkpoint. The 600 MB upload limit is the same either way.

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

Run without credentials and return one clearly-labeled SAMPLE row without uploading anything. Nothing is downloaded, no connection is opened and nothing is charged. Also auto-enabled when no cookies are supplied (e.g. a keyless try-it run).

## Actor input object example

```json
{
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

Result rows / metadata are stored in the default dataset (one row per item).

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

Generated media/files (video, audio, images, captions) are stored in the default key-value store.

# 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 = {
    "proxyConfiguration": {
        "useApifyProxy": true
    },
    "demoMode": false
};

// Run the Actor and wait for it to finish
const run = await client.actor("dami_studio/instagram-reel-uploader-pro").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 = {
    "proxyConfiguration": { "useApifyProxy": True },
    "demoMode": False,
}

# Run the Actor and wait for it to finish
run = client.actor("dami_studio/instagram-reel-uploader-pro").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 '{
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "demoMode": false
}' |
apify call dami_studio/instagram-reel-uploader-pro --silent --output-dataset

```

## MCP server setup

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

```

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/Nv8zp50ASu6bAmkot/builds/h9wm9ujLTBXMFHQ7I/openapi.json
