# Instagram Reels Keyword Search Scraper — Reels Search to Text (`steadyfetch/instagram-keyword-reels-scraper`) Actor

Instagram reels keyword search: type a keyword and get the matching reels with captions, engagement, media links and owner — plus the spoken words of each reel as a transcript, on request. No login. From $0.60 per 1,000 reels, transcripts $0.006 each. An undelivered reel is never charged.

- **URL**: https://apify.com/steadyfetch/instagram-keyword-reels-scraper.md
- **Developed by:** [Steadyfetch Team](https://apify.com/steadyfetch) (community)
- **Categories:** Social media, Automation, Videos
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.60 / 1,000 reels

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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 Reels Keyword Search Scraper — Reels Search to Text

Type the phrases you want and get the reels Instagram ranks top for each: caption, hashtags, mentions, engagement, media links, owner and the permalink — **and, on request, the spoken words of every reel as a timestamped transcript**. No login, no cookies, no proxy to configure. **Your result limit is exact — set 30 and you get 30**, and a reel we could not deliver is never charged. A transcript is charged only when it was actually produced. Nothing is charged for starting a run.

**Using an AI agent?** Pin this actor in Apify's MCP server with one link: `https://mcp.apify.com?tools=steadyfetch/instagram-keyword-reels-scraper`

- **Actor id:** `steadyfetch/instagram-keyword-reels-scraper`
- **Input:** `{ "keywords": ["sourdough bread"], "includeTranscripts": true, "resultsLimit": 30 }` — `"keywords"` is the one field you have to set; `"includeTranscripts"` adds the spoken words.
- **Cap the bill:** set `maxTotalChargeUsd` on the run (a run option, not Actor input), e.g. `0.50` — the run stops when it reaches it, and the last row says what it did not deliver.
- **Price:** from **$0.60 per 1,000 reels** on Gold and above, $2.40 per 1,000 on the Apify free plan; a transcript is a flat $0.006, and only when one was actually produced. No start fee, and a reel we could not deliver is never charged. Full table below.
- **How often it changes:** Instagram re-ranks a keyword continuously, so the same phrase tomorrow is mostly reels you have not seen — and the ones you already have are skipped rather than charged again, so a daily schedule pays only for what is new.

*Unofficial. Not affiliated with, endorsed by, or sponsored by Instagram or Meta Platforms, Inc.*

### What you get

One row per reel, with everything the reel exposes at one price — there is no second charge for "details":

| Column | What it holds |
|---|---|
| `shortCode` · `url` | the reel's own code and its permalink |
| `caption` · `hashtags` · `mentions` | the caption, and the tags and @handles pulled out of it |
| `takenAt` | when it was posted, ISO-8601 UTC |
| `productType` · `isVideo` · `hasAudio` | it is a reel (`clips`), and whether the video carries an audio track |
| `likeCount` · `commentCount` · `playCount` · `videoDuration` | engagement, and the length of the reel |
| `ownerUsername` · `ownerFullName` · `ownerId` · `ownerIsVerified` | who posted it |
| `displayUrl` · `videoUrl` · `videoUrlExpiresAt` | the media links, and when the signed link stops working |
| `transcript` · `transcriptLanguage` · `transcriptSegments` | the spoken words, the language detected, and the timestamped segments |
| `transcriptStatus` · `transcriptReason` · `transcriptCharged` | whether a transcript was produced, why not when it was not, and whether it billed |
| `keyword` · `input` · `source` | which of your phrases found it (`source` is always `instagram`) |
| `charged` · `chargedEvents` · `status` · `statusReason` | whether this row was billed and for which events, so your invoice reconciles from the dataset itself |

Media links are signed and time-limited by Instagram: `videoUrlExpiresAt` tells you exactly how long each one stays fetchable, so download what you need in the same session. The requesting network address is stripped out of every link before it reaches your dataset.

### What a row looks like

One delivered row from a real run — the phrase `latte art`, 2026-09-12. This row charges one `reel`; the transcript is a second event and this run did not ask for one, so `transcriptCharged` is `false`. Media links are signed and time-limited, so they are shown here as placeholders.

```json
{
  "postId": "3975943623665444663",
  "shortCode": "DctY6kHjuc3",
  "url": "https://www.instagram.com/p/DctY6kHjuc3/",
  "type": "video",
  "productType": "clips",
  "caption": "#learnbarista #barista #LearnWithBBTA #CareerGrowth #baristaacademy #baristacourse #baristaskill #professionalbaristacourse #coffeepassion #baristatrainingdhaka #latteart #BaristaLife",
  "hashtags": [
    "learnbarista",
    "barista",
    "learnwithbbta",
    "careergrowth",
    "baristaacademy",
    "baristacourse",
    "baristaskill",
    "professionalbaristacourse",
    "coffeepassion",
    "baristatrainingdhaka",
    "latteart",
    "baristalife"
  ],
  "mentions": [],
  "takenAt": "2026-08-31T15:17:49.000Z",
  "takenAtTimestamp": 1788189469,
  "likeCount": 44379,
  "commentCount": 50,
  "playCount": 2947422,
  "videoDuration": 21.799999237060547,
  "displayUrl": "https://…",
  "videoUrl": "https://…",
  "videoUrlExpiresAt": "2026-09-14T07:11:13.000Z",
  "carouselCount": null,
  "dimensionsWidth": 1080,
  "dimensionsHeight": 1920,
  "ownerId": null,
  "ownerUsername": "bbta.bd",
  "ownerFullName": "Bangladesh Barista Training Academy-BBTA",
  "ownerIsVerified": false,
  "taggedUsers": [],
  "coauthors": [],
  "locationName": null,
  "locationId": null,
  "isPinned": false,
  "isSponsored": false,
  "postSource": "keyword-search",
  "profileHandle": null,
  "profileFullName": null,
  "profileFollowers": null,
  "profileIsVerified": null,
  "profilePostCount": null,
  "source": "instagram",
  "keyword": "latte art",
  "isVideo": true,
  "hasAudio": true,
  "transcript": null,
  "transcriptLanguage": null,
  "transcriptDurationSeconds": null,
  "transcriptSegments": null,
  "transcriptStatus": "not_requested",
  "transcriptRetryable": false,
  "transcriptReason": "Transcripts were not requested on this run — switch on \"Include transcripts\" to add the spoken words.",
  "transcriptCharged": false,
  "input": "latte art",
  "sourceIndex": 0,
  "scrapedAt": "2026-09-12T20:43:40.500Z",
  "repeat": false,
  "charged": true,
  "chargedEvents": {"reel":1,"reel-transcript":0},
  "status": "delivered",
  "retryable": false,
  "statusReason": null,
  "#k": "3975943623665444663",
  "#ce": {"reel":1},
  "#ask": {"delivered":1}
}
```

### Price

Two charged events, and each lands only on work that was actually delivered.

**`reel`** — one per delivered reel row, tiered by your Apify plan:

| Apify plan | Per reel | Per 1,000 reels |
|---|---|---|
| Free plan | $0.0024 | $2.40 |
| Bronze | $0.0014 | $1.40 |
| Silver | $0.0009 | $0.90 |
| Gold and above | $0.0006 | **$0.60 per 1,000 reels** |

**`reel-transcript`** — $0.006 per reel whose spoken words were actually produced, the same on every plan. It is off by default; switch on **Include transcripts** to get it. A reel with no speech, no audio track, an expired media link, or longer than the 180-second transcript ceiling is still delivered as a `reel` row with an explicit null transcript and a reason — and no transcript charge.

Platform usage is included in both prices — there is no separate compute bill on top, and no start fee. Set `maxTotalChargeUsd` on the run and it is a hard ceiling: with transcripts on, a reel and its transcript are reserved together, so the run stops cleanly under your maximum rather than half-billing a unit.

### Honest rows: what "not charged" actually means

Every run closes its own books. A reel that was not delivered leaves an uncharged row saying which of seven things happened, and none of them is billed:

- **`no_reels`** — the ask was not filled, and the row says WHICH of three endings closed the search: Instagram had nothing more to give, or this actor stopped at its own ceiling on how deep one search is read, or its own backstop closed a search whose later pages only re-served reels you already have. Instagram's reel search re-ranks rather than paging cleanly, so the last two are bounds of ours over a feed still serving — the row names them as ours and says what you can change, rather than calling a full shelf an empty one.
- **`stopped_at_limit`** — one of your own limits, your maximum total charge, or the run clock stopped it first. The row names which.
- **`vendor_unavailable`** — the read did not go through. Temporary, says nothing about your phrase, and a re-run is the fix. If the search had already delivered reels when it stopped answering, the row opens with how many you were handed and charged for, and asks for the re-run only on the rest.
- **`source_refused`** — Instagram would not run that phrase as a search at all. The refusal is the same every time, so the row says plainly that re-running cannot change it, and there was no result fee.
- **`unsupported_shape`** — Instagram answered in a shape this actor does not read yet. That is on us, not on your phrase; the answer is the same every time, so the row asks you to tell us the phrase rather than asking you to re-run. No result fee.
- **`vendor_budget`** — this actor reached its own monthly collection allowance and stopped rather than collecting more.
- **`user_input`** — a value that is not a search phrase (a profile link, a reel link, several phrases pasted into one line). The row names the mistake and, where another actor of ours takes that value, names that actor too.

On a delivered reel, `transcriptStatus` carries the same honesty for the transcript leg: `no_speech`, `no_audio_stream`, `expired`, `transcript_too_long`, `failed_download`, `asr_failed`, `asr_unavailable`. Each is an explicit null transcript with a reason on the row, and none of them is charged.

The last row of every run is a receipt: delivered, asked for, transcripts produced, what was skipped, what stopped it, and the charged-event totals for both events.

### How often this data changes

Instagram's keyword search re-ranks continuously: the same phrase asked twice an hour apart returns an overlapping but different slice, and popular phrases take new reels every few minutes. Re-running the same phrase therefore buys mostly new reels — but it would also re-buy the ones you already have, so it does not. Every run remembers what it delivered to your account, in the key-value store `ig-keyword-reels-account` in your own Apify account, and a later run skips those: not delivered again, not charged, and the slot goes to the next new reel instead, so you still get the number you asked for. The memory is a key-value store in your own Apify account, so a run started with a scoped API token in restricted-access mode needs key-value store Read, Write and Create permission (Write is what lets the run remember what it delivered; without it every run pays again) or Actor runs set to Full access; without it the run says so on its status line and on an uncharged row, and cannot skip the reels you already have. That makes a daily schedule cost only what is new, and — as long as the token the schedule runs on can open that store — it never re-transcribes a reel you already paid a transcript for. Switch **Include reels you already have** on to get them back anyway, marked `repeat: true` and still uncharged, and use **Skip reels in this dataset** to carry the same memory across accounts.

### Reliability

The reels come from a licensed data feed rather than from scraping Instagram's own web pages, which is why there is nothing to log into and nothing to configure. When that feed cannot answer, the run says so on an uncharged row and finishes successfully — a failed read is never billed and never dressed up as an empty search. The transcripts are produced by a speech-recognition service from the reel's own audio; when it cannot answer, the reel is still delivered and only the transcript is left null.

### Related actors

- One reel you already have the link to, transcribed: **Instagram Reel Transcript Scraper** — https://apify.com/steadyfetch/instagram-reel-transcript-scraper
- Posts and reels under a hashtag: **Instagram Hashtag Scraper** — https://apify.com/steadyfetch/instagram-hashtag-scraper
- One account's own posts: **Instagram Profile Posts Scraper** — https://apify.com/steadyfetch/instagram-profile-posts

### Limits and good manners

**Max reels** and **Max run seconds** take any number you type: this actor delivers at most 2,000 reels in one run and runs for at most an hour (and for at least 30 seconds), so a bigger ask runs at the ceiling instead of being refused, and one uncharged row says what was asked for and what was used.

Only public reels are returned. Instagram's keyword search serves a ranked slice rather than a complete archive — you can see the same surface yourself at https://www.instagram.com/explore/search/keyword/ — so a very deep ask on one phrase runs out around 60-100 distinct reels and the run says so instead of padding. Transcripts are capped at reels of 180 seconds or less. This actor may stop working if Instagram changes how the data is served; if it does, failed items are never charged.

### Support

Something off, or a column you need that is not here? Open an issue on the Issues tab — we usually reply within a couple of hours.

If it earned its keep, a rating helps other buyers find it, and saving the actor keeps it one click away.

# Actor input Schema

## `keywords` (type: `array`):

Search phrases, one per line — "sourdough bread", "van life", "#travel". An Instagram keyword-search link (/explore/search/keyword/?q=...) works too. A phrase that matches nothing is never charged. A profile link or a reel link is refused with a row naming the actor that does take it. Leave it empty and the run returns built-in sample rows instead of collecting anything, so you can see the output shape at no result fee.

## `includeTranscripts` (type: `boolean`):

OFF (default): rows carry the caption and metadata only. ON: every delivered reel also carries `transcript`, `transcriptLanguage` and timestamped `transcriptSegments`, at $0.006 per produced transcript on top of the reel price. Reels longer than 180 seconds are delivered without a transcript and say so.

## `resultsLimit` (type: `integer`):

Hard cap on the reels this run delivers in total. With more than one keyword the limit is split evenly between them, so 60 across 3 phrases collects 20 from each; a phrase that runs short leaves an uncharged row saying so rather than borrowing another phrase's share. Instagram's search serves a ranked slice, so a very deep ask on one phrase runs out around 60-100 distinct reels. This actor delivers at most 2,000 reels in one run: ask for more and the run continues at 2,000, with one uncharged row saying so.

## `maxRunSeconds` (type: `integer`):

The run stops cleanly before this many seconds and reports what is left, instead of being killed by a timeout. A time limit ends the collecting, never the delivering: reels already in hand are always written out. With transcripts on, budget roughly 4-6 seconds per reel. One run lasts at most 3,600 seconds and needs at least 30: ask outside that and the run continues at the nearest of the two, with one uncharged row saying so.

## `includeSeen` (type: `boolean`):

OFF (default): a reel already delivered to your account is skipped — no row, no charge — and the next new reel takes its place, so you still get the number you asked for. ON: those reels are delivered again anyway, marked `repeat: true`, without a transcript leg and still not charged. Reels older than 90 days in your account's memory count as new again either way.

## `resumeFromDatasetId` (type: `string`):

The dataset ID of a previous run of this actor. Reels already in it are treated as reels you already have and are skipped, so a follow-up run collects only what is new even from a different Apify account. Selecting it here is what grants this run permission to read it.

## Actor input object example

```json
{
  "keywords": [],
  "includeTranscripts": false,
  "resultsLimit": 30,
  "maxRunSeconds": 600,
  "includeSeen": false
}
```

# Actor output Schema

## `reels` (type: `string`):

One row per reel delivered: caption, hashtags, mentions, engagement, media links, owner and the permalink — and, with transcripts on, the spoken words with language and timestamped segments. Two events, two flags on every row: `charged` is the `reel` event (one per delivered reel, everything in the row at that one price); `transcriptCharged` is the `reel-transcript` event, and it is true only on a row whose `transcriptStatus` is `transcribed`. A reel with no speech, no audio track, an expired media link, a speech-service problem or a length over 180 seconds is delivered with `transcript: null`, its `transcriptStatus` naming which, and no transcript charge. Reels your account already had are skipped before they take a slot and are never charged again — that check reads a key-value store in your own account, so a run on a scoped API token without key-value store permission cannot make it and says so on its status line and on an uncharged row.

## `misses` (type: `string`):

One uncharged row for every phrase that returned less than it was asked for, saying which of six things happened: `no_reels` (the search did not fill the ask — Instagram had nothing more to give, or this actor's own read depth or repeat backstop closed the search; the row says which), `stopped_at_limit` (one of your own limits or the run clock stopped it first), `vendor_unavailable` (the read failed and a re-run is the fix), `source_refused` (Instagram would not run that phrase as a search — the same answer every time, so a re-run cannot change it), `unsupported_shape` (Instagram answered in a shape this actor does not read yet — on us, and also the same every time), `vendor_budget` (this actor reached its own monthly collection allowance). A limit you typed above this actor's own ceiling adds one `input_note` row instead of refusing the run, saying what was asked and what was used. None of these is charged.

## `summary` (type: `string`):

Reels delivered, transcripts produced, what was asked for, reels your account already had, reels that appeared under more than one phrase, what stopped the run, the charged-event totals for `reel` and `reel-transcript`, and — when this run's token could not open the repeat memory — `repeatCheck: "unavailable_scope"`.

# 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 = {
    "keywords": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("steadyfetch/instagram-keyword-reels-scraper").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 = { "keywords": [] }

# Run the Actor and wait for it to finish
run = client.actor("steadyfetch/instagram-keyword-reels-scraper").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 '{
  "keywords": []
}' |
apify call steadyfetch/instagram-keyword-reels-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,steadyfetch/instagram-keyword-reels-scraper"
        }
    }
}
```

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/XGZ2GdCZqcF8YqbTS/builds/RE2Jb99oOhhPi0OvR/openapi.json
