# Instagram Search Scraper (`lance_api/instagram-search-scraper-api`) Actor

Search Instagram by keyword for profiles, Reels, hashtags, places, music, and exact-hashtag content. Get structured, deduplicated results with automatic pagination.

- **URL**: https://apify.com/lance_api/instagram-search-scraper-api.md
- **Developed by:** [LanceAPI](https://apify.com/lance_api) (community)
- **Features:** Limited permissions
- **Stats:** 15 total users, 8 monthly users, 78.8% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 results

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?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

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

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Instagram Search Scraper API

**Search profiles, top results, Reels, hashtags, places, music and exact-hashtag posts in one structured workflow — with run-wide de-duplication and predictable pay-per-event billing.**

Search Instagram by keyword and get structured, de-duplicated data back. Seven search modes: **profile search**, **top results** (the media Instagram surfaces for a keyword), **Reel search**, **hashtag search**, **hashtag posts**, **Instagram place search** and **Instagram music search** — pick one per run, so a keyword means one search and one result shape.

Give it a list of queries, choose the kind of result you want, and it returns one dataset. A profile found by three different queries is delivered once, with all three queries listed on the row, so the file you download is already the file you would have built by hand.

- **Seven search modes** — profiles, top results, Reels, hashtags, places, music and exact-hashtag posts.
- **Optional full profile data** — turn it on for a profile search and each account also carries its exact follower, following and post counts, bio, links, category and business flags. Off by default.
- **Batch up to 200 canonical searches** and return up to 3,000 unique rows.
- **Duplicate protection** — if several queries discover the same result, it is delivered and billed once.
- **No Instagram login, cookies or session required.**
- **Predictable billing** — one Search charge when a canonical search starts, then Result charges only for unique delivered rows.

`Top results` is the widest keyword content search: it returns whatever media Instagram's own top tab holds for the keyword, which in practice is mostly Reels with the occasional image or carousel post. `Reels` is the narrower one — the same keyword, restricted to the Reels tab. Ordinary image and carousel posts are found most reliably through exact-hashtag posts (`Hashtag posts`) rather than any keyword search — the details are in [Limits](#limits).

### Quick Start

A first run needs nothing more than a keyword and the kinds of results you want:

```json
{
  "queries": ["travel"],
  "searchType": "content",
  "maxResultsPerQueryType": 20
}
```

1. Paste that input in the Actor's **Input** tab.
2. Click **Start**.
3. Open the run's **Dataset** when it finishes.
4. Each unique result is one structured row. When several searches found the same result, `matchedQueries` and `matches` list all of them on that single row.

For a first run, keep `maxTotalResults` at its default. `hashtagTab` only applies when you pick **Hashtag posts**.

#### What a row looks like

Illustrative example values — invented for illustration, not taken from a run.

```json
{
  "resultType": "profile",
  "query": "openai",
  "matchedQueries": ["openai", "artificial intelligence"],
  "url": "https://www.instagram.com/openai/",
  "id": "1234567890",
  "username": "openai",
  "fullName": "OpenAI",
  "followersCount": 4100000,
  "isVerified": true,
  "fullProfileIncluded": false,
  "rank": 1,
  "scrapedAt": "2026-01-14T09:12:44.512Z"
}
```

The full field reference is in [Output](#output).

### Choose the right search type

There are seven search types. Each one reads a different kind of query and returns a different kind of result, so pick the one that matches what you are looking for — the wrong type is the most common way a first run fails or comes back empty.

| Search type | What to search | Example input | What you get |
|---|---|---|---|
| Profiles (`profiles`) | A username or an account-related keyword | `@openai` | Account records: username, full name, id, profile URL and picture, verification and privacy flags |
| Top results (`content`) | A keyword or search phrase | `hiring freelance developers` | The media Instagram's top tab holds for the keyword — mostly Reels, sometimes image or carousel posts |
| Reels (`reels`) | A video topic, keyword or phrase | `AI video generator` | Reels only: shortcode, caption, video URL, play and view counts, owner |
| Hashtags (`hashtags`) | A topic or keyword to discover hashtags for | `travel photography` | Hashtag records: name, URL, post count |
| Places (`places`) | A city, landmark or venue name | `Central Park` | Place records: id, name, address, city, coordinates |
| Music (`music`) | A song, artist or music keyword | `lofi beats` | Track records: title, artist, duration, cover, preview audio |
| Hashtag posts (`hashtag_content`) | One exact hashtag name, no spaces | `#freelance` | Everything under that hashtag — image, carousel and Reel posts |

Every one of these examples is a valid input for its type. Each query is treated as an independent search, and all of them are case-insensitive and de-duplicated across the run.

#### Hashtags vs. Hashtag posts

These two sound alike but do opposite jobs.

- **Hashtags** is *discovery*: you give it a topic and it finds the hashtags that relate to it. `travel photography` returns a list of hashtag records, so you can see which tags exist and how much content sits under each.
- **Hashtag posts** is *depth*: you already know one exact hashtag, and you want the posts inside it. `#travelphotography` returns the media itself, not a list of tags.

Rule of thumb: to *find* hashtags, use **Hashtags**; to *read the posts under one you already know*, use **Hashtag posts**.

#### Top results vs. Reels

Both take a keyword, but they read two different lists, not two filters over one list.

- **Top results** reads Instagram's own top tab for the keyword — the widest net. In practice it returns mostly Reels, with the occasional image or carousel post.
- **Reels** reads only the Reels tab — the same keyword narrowed to short videos.

A short or ambiguous keyword can return nothing on the Reels tab while the top tab still has a full page, so **Top results** is the safer default when you are not sure.

#### Profiles vs. content searches

Profiles finds *accounts*; `content`, `reels`, `hashtags`, `places` and `music` find *other kinds of results*.

- **Profiles** returns account records — who the accounts are, their follower counts, verification and privacy flags. Use it when you know a topic but not the handles.
- **Top results / Reels / Hashtag posts** return *media* — posts and videos. Use them when you want content, not accounts.

A leading `@` on a Profiles query is removed automatically, so `@openai` and `openai` find the same accounts.

#### JSON examples for each type

Each search type is one minimal input. Only `queries` and `searchType` are required; every other field falls back to its default.

**Profiles**

```json
{ "queries": ["@openai"], "searchType": "profiles" }
```

**Top results**

```json
{ "queries": ["hiring freelance developers"], "searchType": "content" }
```

**Reels**

```json
{ "queries": ["AI video generator"], "searchType": "reels" }
```

**Hashtags**

```json
{ "queries": ["travel photography"], "searchType": "hashtags" }
```

**Places**

```json
{ "queries": ["Central Park"], "searchType": "places" }
```

**Music**

```json
{ "queries": ["lofi beats"], "searchType": "music" }
```

**Hashtag posts**

```json
{ "queries": ["#freelance"], "searchType": "hashtag_content" }
```

You can send several queries at once — each one runs as its own search under the chosen type:

```json
{ "queries": ["@openai", "fitness coach", "nike"], "searchType": "profiles" }
```

### How it works

1. Submit queries and choose the search type you want.
2. The Actor validates and normalises the input, and removes duplicate search operations.
3. Valid searches run and paginate where that search mode supports pagination.
4. Results are normalised and de-duplicated across the whole run.
5. If you asked for full profile data, each account found is read once and its profile fields are merged onto the row that already exists. A read that fails leaves the row as it was.
6. Unique rows are written to the Dataset with query and source-match context for downstream analysis.

### What you can search

| You want | Search type | What comes back |
|---|---|---|
| Accounts matching a keyword | Profiles (`profiles`) | Username, full name, id, profile URL and picture, verification and privacy flags — plus the full public profile of each account when you turn on `includeFullProfile` |
| The media Instagram surfaces for a keyword | Top results (`content`) | Top results: mostly Reels, sometimes image or carousel posts — shortcode, caption, media type, counts, owner |
| Reels matching a keyword | Reels (`reels`) | Shortcode, caption, video URL, play and view counts, owner |
| Hashtags matching a keyword | Hashtags (`hashtags`) | Hashtag name, URL, post count |
| Places matching a keyword | Places (`places`) | Place id, name, address, city, coordinates |
| Music tracks matching a keyword | Music (`music`) | Track title, artist, duration, cover, preview audio |
| Posts under an exact hashtag — the image and carousel entry point | Hashtag posts (`hashtag_content`) | Everything carrying that hashtag (image, carousel and Reel posts), read from the tab you choose |

`Hashtag posts` is the only type that needs an exact hashtag rather than free text, and the only one where you choose an ordering: `top`, `recent`, or `reels`. See [Choose the right search type](#choose-the-right-search-type) for how it differs from `hashtags`.

**One type per run.** You pick a single search type, so one keyword means one search and one result shape — and the bill is one search, not one per type. Want a second type? Run the Actor again; it is a second run, not a second charge on the first.

### Use cases

Each search type answers a different question, so it helps to know which one you want before you run anything:

- **Find accounts by keyword** — search `profiles` for a topic and get the accounts Instagram surfaces for it, with follower counts, so you can shortlist creators or brands. This is the way in when you know the topic but not the handles.
- **See everything a keyword surfaces** — search `content` for the widest net: the top results Instagram itself shows for that word, which mixes Reels with the occasional image or carousel post. Start here when you do not yet know what kind of media a topic lives in.
- **See only the videos a keyword surfaces** — search `reels` to see which videos are being surfaced for a topic, with views, plays, likes and the video asset.
- **Read the posts behind an exact hashtag** — search `hashtag_content` on a single hashtag and page through its feed. This is the search type that returns ordinary image and carousel posts most reliably.
- **Hashtag research before a campaign** — search `hashtags` to see which hashtags exist around a topic and how much content sits under each.
- **Location and local-business research** — search `places` to resolve a city, district or venue into a record with coordinates.
- **Music and audio discovery** — search `music` to find the tracks and artists behind a genre, mood or campaign.
- **Topic sweep** — run the Actor once per search type on the same keyword to get a first-pass picture before narrowing down. Each run is billed separately.
- **Seed lists for downstream analysis** — hand the output to a profile scraper, a Reel scraper or an audience scraper when you need depth on what you just found.

The Actor collects and structures the data. Analysis on top of it — sentiment, scoring, influencer ranking, outreach decisions — happens in your own tools; this Actor does not perform it.

### Input

```json
{
  "queries": ["travel", "specialty coffee", "@openai", "#vanlife"],
  "searchType": "content",
  "maxResultsPerQueryType": 50,
  "maxTotalResults": 1000
}
```

| Field | Type | Required | Default | Prefill | Notes |
|---|---|---|---|---|---|
| `queries` | string list | **Yes** | — | `["travel"]` | Keywords, usernames, hashtags, place names or music terms. One per line in the Console. |
| `searchType` | select | **Yes** | — | `content` | One search type per run. Console labels: Profiles, Top results, Reels, Hashtags, Places, Music, Hashtag posts. API values: `profiles`, `content`, `reels`, `hashtags`, `places`, `music`, `hashtag_content`. |
| `includeFullProfile` | boolean | No | `false` | — | **Full profile data.** Only applies when `searchType` is `profiles`: each account found is also read for its public profile fields. Every other search type ignores it, and the run summary says so. |
| `maxResultsPerQueryType` | integer, 1–200 | No | `50` | — | Upper bound per search; see [Limits](#limits). |
| `maxTotalResults` | integer, 1–3000 | No | `1000` | — | Run-wide ceiling on delivered rows. |
| `hashtagTab` | select | No | `recent` | — | Only used by **Hashtag posts** (`hashtag_content`): `top`, `recent` or `reels`. Every other search type ignores it; if you picked a tab other than the default, the run summary says the choice did not apply. |

Those six fields are the whole input. There is no second, similarly named field next to them.

**Required means required.** `queries` and `searchType` have no default, and the platform checks both before the Actor starts: a request that omits either one is rejected outright, the run never begins, and nothing is charged. The Console pre-fills them so that a first run is one click, but a pre-filled field is not a default.

The legacy `searchTypes` array is still accepted for API compatibility — older integrations send it, and it takes precedence when present. It is no longer part of the public input schema, so it cannot be picked in the form any more; only code written before the single-type input existed sends it. If your integration still uses it, also send `searchType` with any one of its values: the platform requires that field to be there, and the array still decides what actually runs. See [Batch size](#batch-size).

**The run summary reports what actually ran.** `RUN_SUMMARY` in the run's Key-Value store lists the effective search type, the result ceilings that were applied, and a `notes` list for anything that differed from what you asked for — a `hashtagTab` you set to something other than the default on a run that searched no hashtag posts, a limit that had to be pulled inside its range, or a run that came in through the legacy `searchTypes` array. Nothing this Actor changes is changed silently.

#### Full profile data

`includeFullProfile` turns a profile search into a search plus one profile read per account. It is off by default, and it only does anything for `searchType: "profiles"`.

```json
{
  "queries": ["ai"],
  "searchType": "profiles",
  "maxResultsPerQueryType": 20,
  "includeFullProfile": true
}
```

What it adds to each account row: `biography`, `bioLinks`, `externalUrl`, `followersCount`, `followingCount`, `postsCount`, `category`, `isVerified`, `isPrivate`, `isBusinessAccount`, `isProfessionalAccount`, `profilePicUrlHD`, `highlightReelCount`, `joinedRecently` — and `fullProfileIncluded: true` to say the row really was enriched.

Four things it does **not** do:

- **It does not add rows.** One account is still one row; the profile fields land on the row the search already produced.
- **It does not overwrite good data with nothing.** When the profile read has no value for a field — a bio the account does not have, a count a source does not publish — the row keeps what the search already knew. `null` in a profile read means "not published", not "empty".
- **It does not fail the run.** If a profile cannot be read, the account is still delivered with its search fields, `fullProfileIncluded` stays `false`, and nothing extra is charged for it. A run where some reads fail is still a successful search — the summary reports how many were enriched and how many were not.
- **It does not include recent posts.** The profile read is the profile, not its media. Use a profile-posts Actor for that.

Two things worth knowing before turning it on:

- **It costs more.** Each account whose profile data was successfully added carries one **Full Profile** charge on top of the Search and Result charges. Accounts that could not be enriched are not charged for. See [Pricing](#pricing).
- **It is slower.** Each account needs one extra request, done a few at a time so a large batch stays stable. A search of 200 accounts with this on takes longer than the same search with it off.

`includeFullProfile` on a run that searched hashtags, Reels or anything other than profiles is not an error: the flag is ignored, the summary says so, and nothing is charged for it.

### How queries are read

The same word means different things to different searches, so normalisation is per search type rather than one rule for everything.

| Search type | What happens to the query | Example |
|---|---|---|
| `profiles` | A leading `@` is removed | `@openai` → `openai` |
| `hashtags` | A leading `#` is removed; the rest is free text | `#travel` → `travel` |
| `hashtag_content` | A leading `#` is removed, the result is lowercased, and it must be exactly one valid hashtag | `#VanLife` → `vanlife` |
| `content` | Used exactly as typed | `coffee shop` stays `coffee shop` |
| `reels` | Used exactly as typed | `travel photography` stays `travel photography` |
| `places` | Used exactly as typed | `New York` stays `New York` |
| `music` | Used exactly as typed | `lo-fi beats` stays `lo-fi beats` |

- **Duplicates are merged.** Queries that become identical after normalisation are collapsed, and the run summary reports how many were merged.
- **Exact hashtags are strict on purpose.** A hashtag cannot contain a space, so `#new york` is not a hashtag; that one search is skipped instead of being silently rewritten into `newyork`, which would search for something you never asked for. The same query still runs for the other types you selected, and the summary reports how many searches were dropped.

### Batch size

With one search type per run, a run accepts at most **200 canonical search operations**, counted after query normalisation, type validation and duplicate removal — which, with a single type, means at most 200 queries.

| Queries | Search types | Canonical searches |
|---|---|---|
| 200 | 1 | 200 |
| 100 | 1 | 100 |
| 40 | 1 | 40 |

The legacy `searchTypes` array can still name several types when it is sent by code, and then the same 200-operation ceiling applies across all of them: 33 queries × 6 types is 198, 28 × 7 is 196.

A batch that expands past 200 is rejected **before** any data is fetched, and the message states the count you asked for. The operation limit is validated before any search starts, so an oversized batch creates no Search or Result charges. The standard Actor Start charge may still apply to the run.

### Output

One row per unique result — profile, Reel, post, hashtag, place or music track. Every row has the same shape, so the dataset loads straight into a spreadsheet or a database. Missing values follow one rule: a value the source did not report — a number, a name, a URL — is `null`, never `0`, an empty string or `"N/A"`; a list the source reported as empty is `[]`, never `null`. So `hashtags`, `mentions`, `matchedQueries` and `matches` are always arrays, and are empty arrays rather than `null` when there is nothing in them.

#### Main fields

| Field | Meaning |
|---|---|
| `resultType` | One of `profile`, `reel`, `post`, `hashtag`, `place`, `music`. Filter on this to split the dataset by type. |
| `query` | The query that found this row, after normalisation. |
| `matchedQueries` | Every query that found it. A row found by three queries is still one row. |
| `url` | Canonical Instagram URL. `null` for music, which has no page of its own. |
| `rank` | Position in the delivered dataset. |
| `sourceRank` | Position this result had in the source search result when first discovered. |
| `matches` | Every discovery that pointed at this row — query, search type and position in that result list. |
| `scrapedAt` | When the run started, ISO 8601 in UTC. Every row in a run carries the same value, so a dataset can be tied back to the moment it was collected. |
| `fullProfileIncluded` | `true` when this account's row also carries data from a full profile read, `false` otherwise — including every row from a run where `includeFullProfile` was off. |

Type-specific fields: `id`, `username`, `fullName`, `biography`, `bioLinks`, `followersCount`, `followingCount`, `postsCount`, `isVerified`, `isPrivate`, `isBusiness`, `isBusinessAccount`, `isProfessionalAccount`, `category`, `externalUrl`, `profilePicUrl`, `profilePicUrlHD`, `highlightReelCount`, `joinedRecently` for accounts; `shortcode`, `caption`, `videoUrl`, `thumbnailUrl`, `playCount`, `viewCount`, `likeCount`, `commentCount`, `mediaType`, `publishedAt`, `ownerUsername`, `ownerId`, `hashtags`, `mentions`, `locationId`, `locationName`, `musicTitle`, `musicArtist` for Reels and posts; `name`, `mediaCount` for hashtags; `name`, `address`, `city`, `shortName`, `latitude`, `longitude` for places; `title`, `artist`, `durationSeconds`, `coverUrl`, `audioUrl`, `previewUrl`, `explicit`, `audioClusterId`, `audioAssetId` for music.

**Two business flags, not one.** `isBusinessAccount` is Instagram's own account-level business flag. `isProfessionalAccount` is its professional-account flag; an account can be one, the other, both or neither. `isBusiness` is kept for older integrations and always carries the same value as `isBusinessAccount` — it is never a separate signal, and it is never `true` because a source said something adjacent (`is_business`) that does not mean this.

#### Views in the Dataset tab

The Dataset tab offers seven column layouts over the same rows, so the fields that matter for your search type come first instead of being buried behind twenty empty columns.

| View | Leads with | Best for |
|---|---|---|
| `Overview` | type, query, the name columns, caption, followers, likes, views | a run that mixes several search types |
| `Profiles` | username, full name, id, followers, posts, following, verified, private, business, professional, category, bio | account searches, with or without full profile data |
| `Reels` | owner, caption, views, plays, likes, duration, published | Reel searches |
| `Hashtag posts` | owner, caption, likes, comments, media type, published | exact-hashtag feeds |
| `Hashtags` | hashtag, posts with hashtag | hashtag discovery |
| `Places` | place, city, address, coordinates | place searches |
| `Music` | track, artist, duration, cover, preview | audio searches |

Two things a view does not do. It does not filter rows — a Reels run and a profile run in the same dataset share one table, and the type column tells you which is which. And it does not change an export: a JSON, CSV or Excel download always contains all 63 fields regardless of the view you were looking at.

#### Ordering: hashtag tabs, `rank`, `sourceRank` and `matches`

Four different things, deliberately kept apart:

- **`hashtagTab` chooses the source feed.** `top`, `recent` and `reels` are three different lists of posts, not three sortings of one list. Changing the tab changes what exists to be found.
- **`sourceRank` preserves the source order for that one search.** It is the position the result had in the list the source returned, so a Reel that was third for `#vanlife` keeps `sourceRank: 3` on its row.
- **`rank` is run-level delivery order** after global de-duplication — the order of rows in your dataset, not an Instagram ranking and not a per-hashtag order.
- **`matches[]` preserves per-query positions.** When one row was found by `#vanlife` in `recent` and by the keyword `van` in `reels`, each discovery keeps its own `query`, `searchType` and `sourceRank`.

A dataset mixing several hashtags, tabs and search types is therefore not sorted by any single tab. If you need "what the `top` tab of one hashtag looked like", filter that one search out of `matches` and sort by `sourceRank`.

#### Run summary

Every run also writes a `RUN_SUMMARY` record with:

- an `outcome` for the whole run — `SUCCESS`, `PARTIAL`, `NO_DATA` or `FAILED` — so one word tells you whether the run delivered everything it promised;
- how many queries were supplied, used, merged and rejected, and how many search operations ran;
- how many rows were saved, how many duplicates were removed, and the count per result type;
- an outcome for every search — including the ones that returned nothing and why;
- what was billed: searches started, searches charged, unique results delivered, results charged, and whether the two halves agree;
- when `includeFullProfile` was on: how many accounts were asked about, how many were enriched, how many were not, and how many Full Profile charges that produced. A run that searched no profiles says the flag did not apply.

### What each outcome means

| Outcome | What it means | What you get | Is retrying useful? |
|---|---|---|---|
| `SUCCESS` | The search ran and finished normally. | All the results it found. | No need. |
| `PARTIAL` | The search ran and found results, but stopped before it was complete — a result or page limit of this Actor was reached, or the source stopped returning complete pages. | The results it did find; nothing is discarded. | Usually yes, with a narrower query or a higher limit. |
| `TRUNCATED` | The search was not executed. Either the run reached a limit before it could start, or its charge could not be confirmed. It is not a statement that nothing matched. | Nothing from this search. | Yes — raise the limit, or run again. |
| `NO_DATA` | The search completed normally but returned no usable matches from the available search result. | Nothing, because there was nothing usable. | No; try a different keyword or type. |
| `UNAVAILABLE` | The search could not be completed right now. A re-run may well succeed. | Nothing from this search. | Yes. |
| `FAILED` | The search failed for a technical reason. | Nothing from this search. | Yes. |
| `INVALID` | The input for that search was not usable — for example `hashtag_content` with a query that is not a single hashtag. | Nothing from this search. | No; fix the input. |

`queriesWithNoResults` in the summary lists only `NO_DATA` searches — searches that ran and came back with nothing usable. A search that never ran is listed as `TRUNCATED` instead, because "we did not ask" and "we asked and found nothing" are different facts, and only one of them is about Instagram.

These outcomes describe **each search**. `includeFullProfile` never turns a successful search into a failure — but because the profile data is part of what you asked for, the run summary also carries a single `outcome` for the whole run, and that one is `PARTIAL` when some accounts could not be enriched. A run that found 20 accounts and enriched 18 is `outcome: "PARTIAL"`, still delivers all 20 rows, and still reports the search itself as `SUCCESS`. Both facts are shown, because only one of them is about Instagram.

This Actor recovers from common temporary failures on its own, so a single flaky response does not by itself lose you a search. How that is done is internal and is deliberately not exposed in the output.

### Limits

**Keyword searches return almost only video.** That distinction matters, so it is stated plainly instead of being left for you to discover:

- `content` and `reels` are both keyword searches, and both come back overwhelmingly as Reels. In one captured top-search page for a keyword, all 20 items were Reels; a later run on the same keyword returned 18 Reels plus 1 carousel and 1 image post. Posts do appear — they are simply the minority.
- Content under a *specific* hashtag is searchable, images included. A hashtag feed still leans towards video, but it is where image and carousel posts actually appear — roughly one row in ten in the sample this Actor was built against.
- There is no "image posts" search tab on Instagram, so no button here can honestly promise one. Any product that offers "search image posts by keyword" is quietly reading a hashtag or a profile feed, and so is this Actor when you use `hashtag_content`.

If you need image posts, start from a hashtag or an account instead of a keyword: give it a hashtag and it returns whatever that hashtag contains.

Other boundaries worth knowing:

- **One run covers at most 200 canonical searches and 3,000 rows.** Both are upper bounds, not targets.
- **`maxResultsPerQueryType` is a ceiling, not a guarantee.** A search returns fewer results when the source runs out of them, when the result count is genuinely smaller, or when this Actor's own page budget is reached first — in that last case the search is marked `PARTIAL` rather than reported as complete.
- **Hashtag tabs have different depth.** `recent` supports paging through older posts. `top` and `reels` return a single page that cannot be continued, so they are short by nature — a finite, curated set.
- **Large batches can take longer than small ones.** Searches are processed in a controlled way to keep batch execution stable.
- **`content` is the slowest type.** It reads a live result page rather than a cached list, and one page can take up to a minute when Instagram is slow. That is waiting, not an error: the Actor keeps the request open and returns the page. If a page still has not arrived after 75 seconds it is reported as a temporary issue rather than as "no results", so a slow source never masquerades as an empty one.
- **Results reflect the source at the moment you ask.** Search results are not a complete index of Instagram, and the same query on two different days legitimately returns overlapping rows.

### Notes on data quality

- **Missing values stay missing.** If a source does not report a follower count, the field is `null` rather than `0`, so "unknown" and "zero" never look the same in your data.
- **Placeholder text is discarded.** Some sources answer with a sentence such as "Access delayed…" where a name or city belongs; that is recognised and turned into `null` instead of being stored as if it were a real name.
- **Counts are as reported.** Follower counts and other engagement numbers are what Instagram last reported, not live counters.
- **No filler requests.** Search results can be thinner than a profile page: counts are often absent on a keyword search even when the account's own page shows them. The Actor does not make extra requests to fill those gaps silently — the one way to ask for more is `includeFullProfile`, which is explicit, is priced per account, and says `fullProfileIncluded` on every row it touched.

### Pricing

**Pay for searches that actually run and unique results that are actually delivered.**

- **Actor Start — $0.0005 per run.** Applied once by the Apify platform.
- **Search — one charge when one canonical query × result type actually starts.** Additional pages and automatic recovery do not create another Search charge.
- **Result — one charge per unique result successfully delivered.** When several queries discover the same profile, Reel, hashtag, place, post or music item, it is delivered once and creates one Result charge.
- **Full Profile — one charge per account whose profile data was successfully added.** Only when you turn on `includeFullProfile`. An account that could not be enriched, or a row that already had the flag from a different run, creates no Full Profile charge here.

**Results start at $1.00 per 1,000 on eligible plans. Search pricing starts at $2.00 per 1,000 on eligible paid plans. Full Profile data is $1.50 per 1,000 accounts on the Free plan, falling to $1.00 on higher plans.** See the Pricing tab for current plan-specific rates.

Platform usage is currently included under this Actor's active Pricing configuration.

#### What controls cost

- `maxResultsPerQueryType` limits result volume for one search and helps control Result-fee exposure.
- `maxTotalResults` caps unique result rows across the run and helps control Result-fee exposure.
- Selecting fewer query × search-type combinations reduces the number of possible Search charges.
- Duplicate canonical searches are not charged twice.
- Duplicate results are not charged twice.
- An executed search that returns zero usable results can still create one Search charge, but creates no Result charge.
- `includeFullProfile` adds one Full Profile charge per successfully enriched account. Turn it off to keep a profile search at Search + Result only.
- An account that could not be enriched creates no Full Profile charge, even though the row is still delivered.
- Invalid or never-started search operations create no Search charge.

Neither result limit is a guaranteed total-cost cap: Actor Start and Search charges still apply. Use them to bound how much data one run can produce, not as a spending guarantee.

#### Example

**1 run + 10 Search operations + 100 unique Results**

```text
Actor Start       $0.0005
10 Searches       $0.0200
100 Results       $0.1500
--------------------------
Total             $0.1705
```

Example using current Bronze rates. See the Pricing tab for current plan-specific rates.

### API access

Every run follows the same three steps: **start the Actor → wait for the run to finish → read its dataset**. Only the run id and the dataset id tie the steps together, and both come back in the start response.

Authentication is always a bearer header. Tokens are never sent in a query string:

```bash
export APIFY_TOKEN=...   # Settings → Integrations in the Apify Console
export ACTOR_ID=lance_api~instagram-search-scraper-api   # REST form: <owner>~<actor-name>
```

The SDKs take the same identifier with a slash: `lance_api/instagram-search-scraper-api`.

#### cURL

```bash
## 1. Start a run and keep its ids
RUN=$(curl -sS -X POST "https://api.apify.com/v2/acts/$ACTOR_ID/runs" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"queries":["travel"],"searchType":"reels","maxResultsPerQueryType":50}')

RUN_ID=$(printf '%s' "$RUN" | jq -r '.data.id')
DS=$(printf '%s' "$RUN" | jq -r '.data.defaultDatasetId')

## 2. Wait for it to finish
curl -sS "https://api.apify.com/v2/actor-runs/$RUN_ID?waitForFinish=300" \
  -H "Authorization: Bearer $APIFY_TOKEN"

## 3. Read the dataset
curl -sS "https://api.apify.com/v2/datasets/$DS/items?format=json&clean=true" \
  -H "Authorization: Bearer $APIFY_TOKEN"
```

#### JavaScript

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });

// 1 + 2: start the run and wait for it to finish
const run = await client.actor('lance_api/instagram-search-scraper-api').call({
  queries: ['travel', '#vanlife'],
  searchType: 'reels',
  maxResultsPerQueryType: 50,
});

// 3: read the dataset
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.length, items[0]);
```

#### Python

```python
import os
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])

## 1 + 2: start the run and wait for it to finish
run = client.actor("lance_api/instagram-search-scraper-api").call(run_input={
    "queries": ["travel", "#vanlife"],
    "searchType": "reels",
    "maxResultsPerQueryType": 50,
})

## 3: read the dataset
items = client.dataset(run["defaultDatasetId"]).list_items().items
print(len(items), items[0])
```

Both `queries` and the search type are **required** in an API call: send them explicitly. The field is `searchType` (one value), and the platform rejects a request that leaves it out before the run starts, so nothing is charged for a call that forgot it.

The older `searchTypes` array still works and takes precedence when present, so published integrations keep running unchanged — with one addition: because the platform now requires `searchType` to be present, send it alongside the array (any one of the array's own values). The array still decides what runs; `searchType` is only there to satisfy the platform. The run summary names the source, so you can always see which one was used.

### FAQ

**Can I search ordinary image posts by keyword?**
Not reliably. Both keyword types (`content` and `reels`) come back mostly as Reels, with image and carousel posts only as the minority, because that is what Instagram's keyword search holds. To reach image and carousel posts, search an exact hashtag (`hashtag_content`) or a specific account instead. [Limits](#limits) explains why.

**What is the difference between `content` and `reels`?**
Both take a keyword, and they read two different lists. `content` reads Instagram's top results for that word — the widest net, mostly Reels with the occasional image or carousel post. `reels` reads only the Reels tab. A short or ambiguous keyword can return nothing on the Reels tab while the top tab still has a full page, so `content` is the better default.

**What is the difference between `hashtags` and `hashtag_content`?**
`hashtags` takes a topic and discovers which hashtags exist around it — `travel photography` returns hashtag records. `hashtag_content` takes one exact hashtag and returns the posts inside it — `#vanlife` returns media. One is discovery, the other is depth.

**Can I combine several search types?**
Not in one run: the input form takes a single search type, so a keyword means one search and one bill. Run the Actor again for a second type — each run is billed on its own. (Code written before the single-type input existed can still send the legacy `searchTypes` array to search several types in one run; the run summary says when that happened.)

**Why did I get fewer than `maxResultsPerQueryType`?**
Because the number is a ceiling. The source may genuinely have fewer results, may stop returning complete pages, or the Actor's own page budget may be reached first — the run summary says which, and marks the search `PARTIAL` when it was cut short.

**What happens at `maxTotalResults`?**
The dataset stops growing. Searches that have not started are not started and are reported as `TRUNCATED`; a search that was mid-flight keeps what it had and is reported as `PARTIAL`. Raise the limit and run again, or split the batch.

**Are duplicate results returned twice?**
No. De-duplication is run-wide: one row per unique result, with every query that found it in `matchedQueries` and `matches`. Duplicates are billed once for the same reason — one row, one Result charge.

**What does `NO_DATA` mean?**
The search ran and completed, but returned no usable matches from the search result that was available. It does not claim that all of Instagram has nothing for that word — only that this search came back empty.

**Do I need an Instagram login, cookies, or a session?**
No. The Actor needs no Instagram account, no cookies and no session, and it never asks for your Instagram password.

**Which hashtag tabs support pagination?**
`recent` pages through older posts. `top` and `reels` are single-page only, so their result sets are naturally short.

**How is this Actor billed?**
This Actor uses pay-per-event pricing. A Search charge applies once when one canonical query × result type actually starts, and a Result charge applies to each unique result successfully delivered. Additional pages and automatic recovery do not create additional Search charges, and duplicate results are not charged twice. With `includeFullProfile` on, each successfully enriched account adds one Full Profile charge; an account that could not be enriched adds none. A $0.0005 Actor Start charge applies once per run. See the Pricing tab for current plan-specific rates.

**What does `includeFullProfile` actually add?**
The public profile of each account the search found: exact follower, following and post counts, bio, links, category, verification and privacy flags, avatar, highlight count, and the business and professional flags. It does not add rows, does not include recent posts, and does not replace a search row's existing values with empty ones.

**Why is `fullProfileIncluded` `false` on some rows even though I turned it on?**
Each account is read once. When that one read fails — the source timed out, answered with an error, or returned a profile that is not the account that was asked for — the row is still delivered with its search fields and stays `false`, and no Full Profile charge is raised for it. The run summary reports the count, and re-running is the way to retry those accounts.

### Troubleshooting

| Symptom | Meaning |
|---|---|
| Run status `FAILED`, no rows, summary says `UNAVAILABLE` | The search could not be completed at all. Re-run; if it persists, the data source is having an outage. |
| `PARTIAL` outcomes in the summary | Some searches delivered results and some did not, or some stopped early at a limit. The rows that did arrive are in the dataset — nothing is discarded. |
| `TRUNCATED` outcomes in the summary | Those searches never ran: the run hit `maxTotalResults` first. Raise it, or run fewer queries. |
| `BATCH_LIMIT_EXCEEDED` | Queries × search types exceeded 200. The message states the count you asked for. |
| `NO_DATA` for a query you expected results from | The search ran and came back empty. Try a broader keyword, or switch the type — a keyword may match accounts but no hashtags. |
| A hashtag search returns fewer rows than `maxResultsPerQueryType` | The tab you chose does not paginate indefinitely. `top` in particular is a finite, curated set. |
| `INVALID` for one search type only | The query is not usable for that type — usually `hashtag_content` with a phrase instead of a single hashtag. The other types still ran. |
| `fullProfileIncluded: false` on rows you expected to be enriched | Those accounts could not be read on this run. The search rows themselves are complete and were still billed as search results; no Full Profile charge was raised for the ones that failed. Re-run to retry them. |
| `includeFullProfile` was on but the summary says it did not apply | The run searched something other than profiles. The flag only works with `searchType: "profiles"`; nothing was read and nothing extra was charged. |

### Responsible Use

- Collect only public data, or data you are otherwise permitted to collect.
- Follow the laws that apply to you and the terms of the platforms you use.
- Respect privacy and copyright: do not republish personal data or media you have no right to use.
- Do not use this Actor for spam, harassment, or any abusive, deceptive or unlawful purpose.

### Support

- Email: **lanceapi2026@hotmail.com**
- Or open an issue on the Actor page.

A useful bug report contains:

- the **run id** (Console → Runs → the run);
- the input, with anything sensitive removed;
- the visible status or error message;
- what you expected and what you got instead.

Please never send: your Apify token, cookies or session data, passwords, access keys, proxy details, or `Authorization` headers. Nothing in a support conversation needs them, and a message containing them should be treated as compromised.

### Related Actors

Once you know *what* you are interested in, these take it further:

| If you need to... | Recommended Actor |
|---|---|
| inspect everything posted by a discovered account | [Instagram Profile Posts & Reels Scraper](https://apify.com/lance_api/instagram-profile-posts-reels-scraper) |
| inspect one discovered profile in depth | [Instagram Profile Scraper – Followers & Engagement](https://apify.com/lance_api/instagram-profile-scraper) |
| analyse the audience of a discovered account | [Instagram Followers & Following Scraper – Export Lists](https://apify.com/lance_api/instagram-followers-following-scraper) |
| read the comments on a discovered post | [Instagram Comments & Replies Scraper – Posts, Reels](https://apify.com/lance_api/instagram-comments-scraper-api) |
| see who liked a discovered post | [Instagram Likers Scraper – Post & Reel Like](https://apify.com/lance_api/instagram-likers-scraper) |
| download a discovered Reel | [Instagram Reels Downloader – Video, Audio & Metadata](https://apify.com/lance_api/instagram-reels-downloader-api) |
| turn a discovered Reel into text | [Instagram Video Transcript Scraper](https://apify.com/lance_api/instagram-video-transcript-scraper) |
| find the sound behind a discovered Reel | [Instagram Music & Audio Scraper – Reels by Sound](https://apify.com/lance_api/instagram-music-audio-scraper) |

Use this Actor first when you have keywords and no accounts yet; switch to the account-level Actors once you know who you are interested in.

# Actor input Schema

## `queries` (type: `array`):

One or more search terms, one per line. What each term should be depends on the Search type below — Profiles: a username or account keyword, e.g. @openai. Top results: a keyword or phrase, e.g. hiring freelance developers. Reels: a video topic or phrase, e.g. AI video generator. Hashtags: a topic to discover hashtags for, e.g. travel photography. Places: a city, landmark or venue, e.g. Central Park. Music: a song, artist or music keyword, e.g. lofi beats. Hashtag posts: one exact hashtag with no spaces, e.g. #freelance — a phrase with spaces is rejected here, so use Top results or Reels for those. Duplicate terms are merged, and the run summary reports how many.

## `searchType` (type: `string`):

Pick one search mode per run. Profiles — discover accounts for a username or keyword. Top results — the media Instagram surfaces for a keyword (mostly Reels, sometimes image or carousel posts). Reels — only the short videos for a keyword. Hashtags — which #tags relate to a keyword or topic. Hashtag posts — the posts inside one exact hashtag (the reliable way to reach image and carousel posts). Places — locations matching a keyword. Music — tracks matching a keyword. One type per run keeps the result set and the bill to one shape; run the Actor again for a second type.

## `includeFullProfile` (type: `boolean`):

Only applies when Search type is Profiles; every other search type ignores it, and the run summary says so. When on, each matched account is also read for its full public profile — biography, exact follower, following and post counts, business and category details, website, bio links, HD profile picture and highlight count — and written into the same row. A profile that cannot be read still returns its search row, marked as not enriched. Each successfully enriched account costs one extra charge on top of the per-result charge.

## `maxResultsPerQueryType` (type: `integer`):

How many results each search may deliver. 1–200, default 50. An upper bound, not a promise: a search that genuinely has fewer results returns fewer rather than being padded.

## `maxTotalResults` (type: `integer`):

Hard ceiling for the whole run across all queries, 1–3000. When it is reached the remaining searches stop before they start, and the run summary lists them as not executed. A large batch can therefore be cut short by this limit before "Results per search" is filled.

## `hashtagTab` (type: `string`):

Only applies when Search type is Hashtag posts; every other search type ignores this field, and the run summary says so when it happens. Top and Reels return one page that cannot be continued; Recent supports paging through older posts.

## Actor input object example

```json
{
  "queries": [
    "travel"
  ],
  "searchType": "content",
  "includeFullProfile": false,
  "maxResultsPerQueryType": 50,
  "maxTotalResults": 1000,
  "hashtagTab": "recent"
}
```

# Actor output Schema

## `dataset` (type: `string`):

One row per unique result — profile, Reel, post, hashtag, place or music track. A result found by several queries is delivered once and lists every query that found it.

## `datasetConsole` (type: `string`):

No description

## `runSummary` (type: `string`):

Totals for this run: queries and search operations executed, results saved, duplicates removed, outcome counts, results by type, which searches found nothing, which were partial or failed, and whether billing was applied.

# 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 = {
    "queries": [
        "travel"
    ],
    "searchType": "content"
};

// Run the Actor and wait for it to finish
const run = await client.actor("lance_api/instagram-search-scraper-api").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 = {
    "queries": ["travel"],
    "searchType": "content",
}

# Run the Actor and wait for it to finish
run = client.actor("lance_api/instagram-search-scraper-api").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 '{
  "queries": [
    "travel"
  ],
  "searchType": "content"
}' |
apify call lance_api/instagram-search-scraper-api --silent --output-dataset

```

## MCP server setup

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

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/PRd4GvzOJZOucIMYO/builds/BTT5XOESkTwe40Fw2/openapi.json
