Apify is a marketplace of 7,000+ pre-built scrapers (“Actors”), each with its own input and output schema, run on Apify’s infrastructure rather than yours. NativePort proxies one narrow, deliberate slice of Apify’s API: starting an Actor run, waiting for it to finish, and reading the dataset it produced. This guide uses that flow for one purpose: pulling a public Instagram profile’s public data through an official Apify Actor. Nothing here touches a private account, requires a login, or works around Instagram’s own access controls — the Actor’s input schema has no field for credentials or session cookies, only a username.
Why through NativePort
Apify starts at a monthly plan, so one profile scrape costs a whole month of Apify. Here you run the Actor and pay for that run, and there is nothing to cancel afterwards.
What you’ll need
- A NativePort API key. Sign up to get a key and $1 in credits.
- Python 3, standard library only —
urllib.request, nopip install. - Your key exported as an environment variable, never hardcoded:
export NATIVEPORT_API_KEY="np_..."
The route
NativePort admits Apify’s run lifecycle and nothing else: start a run of an Actor (or a saved Actor Task), poll it, read the dataset it produced, or abort it.
POST https://api.nativeport.ai/apify/v2/acts/{actorId}/runs
POST https://api.nativeport.ai/apify/v2/actor-tasks/{taskId}/runs
GET https://api.nativeport.ai/apify/v2/actor-runs/{runId}
GET https://api.nativeport.ai/apify/v2/datasets/{datasetId}/items
POST https://api.nativeport.ai/apify/v2/actor-runs/{runId}/abort
Add waitForFinish=60 to the start and a run that finishes within a minute comes back finished, so a short job takes one call to start it and one to read its dataset.
Note the path segment: it’s /v2/acts/, not /v2/actors/ — Apify’s newer docs lead with /v2/actors/ as the current spelling, but that path isn’t one NativePort admits. Use /v2/acts/ exactly as shown above; anything else 404s at the gateway before it reaches Apify. {actorId} is the tilde-separated publisher and Actor name — apify~instagram-profile-scraper for the Actor this guide uses — or Apify’s own opaque Actor ID. Apify’s account-management surface (usage, proxy password, run and dataset listings, webhooks, schedules) isn’t exposed here at all; the run’s own dataset, read by the id the start hands back, is the only way to get data out, and the run and its dataset answer only to the key that started them.
Pull one public profile
apify/instagram-profile-scraper is Apify’s own official Actor for exactly this: given one or more Instagram usernames, it returns each account’s public profile data — no posts, no comments, the narrowest surface the Actor offers.
import json
import os
import urllib.request
GATEWAY = "https://api.nativeport.ai"
API_KEY = os.environ["NATIVEPORT_API_KEY"]
ACTOR = "apify~instagram-profile-scraper"
TERMINAL = {"SUCCEEDED", "FAILED", "TIMED-OUT", "ABORTED"}
def call(method, path, body=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(GATEWAY + path, data=data, method=method)
req.add_header("Authorization", f"Bearer {API_KEY}")
if data is not None:
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req, timeout=90) as resp:
return json.loads(resp.read())
def run_actor(actor_id, actor_input):
run = call("POST", f"/apify/v2/acts/{actor_id}/runs?waitForFinish=60", actor_input)["data"]
while run["status"] not in TERMINAL:
run = call("GET", f"/apify/v2/actor-runs/{run['id']}?waitForFinish=60")["data"]
if run["status"] != "SUCCEEDED":
raise RuntimeError(f"run {run['id']} ended {run['status']}")
return call("GET", f"/apify/v2/datasets/{run['defaultDatasetId']}/items")
items = run_actor(ACTOR, {"usernames": ["natgeo"]})
profile = items[0]
print(profile["username"], profile["followersCount"], profile["private"])
usernames is the only required field — an array, even for one account. The Actor also takes an optional includeAboutSection boolean (default false), a paid add-on on Apify’s side that adds join date and verification info; leave it off for the minimal, cheapest call. waitForFinish=60 makes the start wait up to a minute for the run to finish (a single-username lookup usually does), and the loop keeps polling, a minute at a time, if it doesn’t. Polling the run and reading its dataset cost nothing; the run itself is billed once, when it ends (see What this costs, below).
The response is a JSON array — one item per requested username — each item shaped like Apify’s own documented dataset schema for this Actor:
[
{
"id": "528817151",
"username": "natgeo",
"url": "https://www.instagram.com/natgeo/",
"fullName": "National Geographic",
"biography": "...",
"followersCount": 280000000,
"followsCount": 150,
"postsCount": 26000,
"private": false,
"verified": true,
"profilePicUrl": "https://...",
"latestPosts": [ { "...": "up to 12 most recent posts, each with its own stats" } ]
}
]
(Values illustrative — the actual counts depend on the account’s state when you run it.) A nonexistent or now-private username comes back as a dataset item carrying error and errorDescription fields instead of profile data, rather than failing the whole call — check for those on each item before trusting the rest of its fields.
A handful of public posts instead of a profile
For post-level data rather than profile summary, swap in apify/instagram-scraper with directUrls pointed at the profile and a small resultsLimit:
items = run_actor("apify~instagram-scraper", {
"directUrls": ["https://www.instagram.com/natgeo/"],
"resultsType": "posts",
"resultsLimit": 5,
})
Keep resultsLimit small (single digits) to start: this Actor is built for large crawls (its own configured timeout is measured in days, not seconds), so a bigger request simply runs longer, with the loop polling until it ends, and costs more, since each post is a billed result. Page a large dataset with limit and offset on the items read. Each returned item is one post: shortCode, caption, hashtags, likesCount (-1 if the creator hid it — not an error), commentsCount, timestamp, ownerUsername, and more.
Public data only — by design, not by promise
Both Actors used here only reach what Instagram already serves without a login — there is no username/password or session-cookie field anywhere in either Actor’s input schema, so there’s no way to point this route at a private account even if you wanted to. From Apify’s own published guidance on these Actors: they “do not extract any private user data” and only surface “what the user has chosen to share publicly.” That data can still be personal data under regulations like GDPR even when it’s public, so scrape with a legitimate reason, and see Apify’s own legality-of-scraping guidance if you’re unsure whether yours qualifies. Neither Instagram nor Apify publishes a fixed numeric rate limit for these Actors — keep request volume and resultsLimit deliberately small, run scrapes on your own schedule rather than in a tight loop, and treat a 429 (below) as a signal to slow down, not retry immediately.
Errors you’ll actually hit
402 Payment Required(from NativePort): the NativePort balance is at $0. Distinct from Apify’s own402below — this one is your balance, checked before the request ever leaves the gateway.401 Unauthorized: theAuthorization: Bearer <NATIVEPORT_API_KEY>header is missing, malformed, or doesn’t resolve to an account.403 Forbidden: the key is valid but the account isn’t active.404on any path outside the five routes listed above, including/apify/v2/actors/...(the newer path spelling Apify’s own docs now lead with), the retiredrun-syncactions, and any PUT/DELETE call, and on a run or dataset id your key didn’t start: not a rate limit, just not a route you can reach.400 invalid-input(from Apify): the JSON body doesn’t match the Actor’s input schema — most often a missing or malformedusernamesarray.404 record-not-found(from Apify):{actorId}doesn’t resolve to a real Actor — check the tilde-separated spelling.- A run that ends
FAILED,TIMED-OUTorABORTED:run_actorraises on it.TIMED-OUTmeans the run hit the Actor’s own time limit; ask for fewer results, or raise the limit with atimeoutquery parameter (in seconds) on the start. 429 rate-limit-exceeded(from Apify): back off before retrying, rather than looping immediately.- A
200/201with anerrorfield inside a dataset item: that one username failed (private, deleted, or never existed) without failing the whole batch — check each item, don’t assume array length means success count.
Security
Treat NATIVEPORT_API_KEY like any other credential: environment variable or secret store, never a literal string in source, never logged. Apify’s own account token never reaches your process — the gateway injects it server-side into every admitted call, and a client-supplied token query parameter (Apify’s alternate auth channel) is stripped rather than honored, so there’s no way to smuggle a different Apify credential through this route.
What this costs
The run is billed once, when it ends, priced from the cost Apify reports for it: the Actor’s per-result charges (both Actors used here are pay-per-result on Apify’s own platform) and the compute behind them. Starting the run, polling it and reading its dataset cost nothing, and a run that genuinely cost nothing is billed nothing. A zero balance answers every request with 402 instead of degrading. How metered prices work: pricing.
Where to go next
- Apify provider page: current pricing and benchmark standing.
- Domain scraping leaderboard: how Apify’s Actor marketplace scores against the field for scrape jobs like this one.
- How to choose a browser API on NativePort: for sites without a pre-built Actor, a one-shot Steel or Browserless call, or a driven Browserbase session, cover the same “get content off a page” job in a different shape.
- Pricing: the full billing model.