NativePort
← How-to

How to Use the Firecrawl API Through NativePort in Python

Scrape a page to clean markdown, then kick off an asynchronous crawl and poll it to completion — the exact Firecrawl v2 actions NativePort's gateway admits, in plain Python.

Firecrawl turns a URL into markdown clean enough to hand a model directly: boilerplate stripped, navigation gone, just the content. NativePort proxies its v2 API under one key and one balance, so the request and response shapes below are Firecrawl’s own — nothing reshaped in between. This guide covers the two request patterns that cover most of what people build against Firecrawl: a single synchronous scrape, and an asynchronous crawl you submit once and poll until it finishes.

What you’ll need

  • A NativePort API key. Sign up to get a key and $5 in credits.
  • Python 3, standard library only — every call below goes through urllib.request, no pip install.
  • Your key exported as an environment variable, never hardcoded:
export NATIVEPORT_API_KEY="np_..."

What’s admitted

Firecrawl’s own API is bigger than what the gateway forwards. /firecrawl/* mounts an allowlist, not a full passthrough:

  • POST /v2/scrape, /v2/crawl, /v2/batch/scrape, /v2/map, /v2/search, /v2/extract — the six action endpoints. Scrape, map and search return their result inline; crawl, batch scrape and extract return a job id to poll.
  • GET /v2/crawl/{id} and /v2/batch/scrape/{id} (plus each one’s /errors sub-resource), and /v2/extract/{id} (no /errors sub-resource for extract) — status by job id.
  • DELETE /v2/crawl/{id} — cancel a running crawl.

Two things Firecrawl documents are deliberately not reachable through this gateway, both blocked because the gateway authenticates every request with one shared company key: /v2/team/* (the account’s credit balance and spend history) and GET /v2/crawl/active (a live enumeration of every in-flight job on that shared account, including other customers’). Both 404 rather than 403 — a blocked path looks identical to one that never existed. Anything outside this list, including any Firecrawl endpoint added after this was written, gets the same 404.

Scrape a page

/v2/scrape is the synchronous case: send a URL, get a result back on the same request.

import json
import os
import urllib.request

GATEWAY = "https://api.nativeport.ai"
API_KEY = os.environ["NATIVEPORT_API_KEY"]


def scrape(url):
    body = json.dumps({"url": url, "formats": ["markdown"]}).encode()
    req = urllib.request.Request(f"{GATEWAY}/firecrawl/v2/scrape", data=body, method="POST")
    req.add_header("Authorization", f"Bearer {API_KEY}")
    req.add_header("Content-Type", "application/json")
    req.add_header("User-Agent", "nativeport-python-guide/1.0")
    with urllib.request.urlopen(req, timeout=60) as resp:
        return json.loads(resp.read())


result = scrape("https://firecrawl.dev")
print(result["data"]["markdown"][:500])
print(result["data"]["metadata"]["title"])

The response is Firecrawl’s own /v2/scrape shape, untouched:

{
  "success": true,
  "data": {
    "markdown": "# Firecrawl\n\nTurn websites into LLM-ready data...",
    "metadata": {
      "title": "Firecrawl",
      "sourceURL": "https://firecrawl.dev",
      "statusCode": 200
    }
  }
}

formats takes more than the string "markdown" — it also accepts option objects, for example {"type": "json", "schema": {...}} to extract structured fields from the same page in the same call, or {"type": "screenshot", "fullPage": true}. Anything Firecrawl’s /v2/scrape documents for formats, onlyMainContent, waitFor, location and the rest of its scrape options is forwarded as-is; the gateway doesn’t narrow the request body, only which endpoints are reachable.

Crawl a site and poll it to completion

/v2/crawl is asynchronous: the initial POST returns a job id immediately, and the pages themselves show up on a status endpoint you poll until the job finishes.

import time


def start_crawl(url, limit=10):
    body = json.dumps({"url": url, "limit": limit}).encode()
    req = urllib.request.Request(f"{GATEWAY}/firecrawl/v2/crawl", data=body, method="POST")
    req.add_header("Authorization", f"Bearer {API_KEY}")
    req.add_header("Content-Type", "application/json")
    req.add_header("User-Agent", "nativeport-python-guide/1.0")
    with urllib.request.urlopen(req, timeout=30) as resp:
        return json.loads(resp.read())["id"]


def poll_crawl(job_id, interval=3):
    req = urllib.request.Request(f"{GATEWAY}/firecrawl/v2/crawl/{job_id}", method="GET")
    req.add_header("Authorization", f"Bearer {API_KEY}")
    req.add_header("User-Agent", "nativeport-python-guide/1.0")
    while True:
        with urllib.request.urlopen(req, timeout=30) as resp:
            status = json.loads(resp.read())
        if status["status"] in ("completed", "failed", "cancelled"):  # terminal
            return status
        time.sleep(interval)


job_id = start_crawl("https://firecrawl.dev", limit=10)
final = poll_crawl(job_id)
print(final["status"], final["completed"], "of", final["total"], "pages")
for page in final["data"]:
    print(page["metadata"]["sourceURL"])

The status body carries status (scraping while running; completed, failed or cancelled once it’s done), a running completed/total page count, creditsUsed (a cumulative total, not a per-poll delta — see costs below), and once the job is completed, a data array of the same per-page shape /v2/scrape returns. A crawl over more pages than fit in one response body paginates via a next URL in the status object; the loop above assumes a small enough limit that this doesn’t come up, but check for next before assuming data is everything.

/v2/batch/scrape and /v2/extract follow the identical submit-then-poll shape, at /v2/batch/scrape/{id} and /v2/extract/{id} respectively — swap the start and poll URLs and the rest of this pattern carries over unchanged.

Errors you’ll actually hit

  • 402 Payment Required: the NativePort balance is at $0. Every request answers this deterministically rather than degrading, so top up and retry.
  • 401 Unauthorized: the Authorization: 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.
  • 404 on /v2/team/* or GET /v2/crawl/active: both are blocked outright, not rate-limited or degraded — see “What’s admitted” above.
  • 404 on a status poll: almost always a wrong or mistyped job id. Firecrawl job ids are unguessable UUIDs handed only to whoever started the job — the gateway doesn’t keep its own per-account ownership table for them the way it does for some other providers’ stateful resources, so the id itself is what stands between one caller’s job and another’s.
  • A status of scraping that never seems to change: not a failure by itself — the job is still running, and completed/total in the same body show progress. Keep polling at a reasonable interval rather than tightening the loop; Firecrawl’s own crawl times scale with page count and target complexity, not with poll frequency.

Security

Treat NATIVEPORT_API_KEY like any other credential: environment variable or secret store, never a literal string in source, never logged. The gateway injects the real Firecrawl credential server-side — your key only ever needs to authenticate to NativePort.

What this costs

Firecrawl’s sync actions (scrape, map, search) meter a flat one credit per call. The async jobs (crawl, batch/scrape, extract) bill twice: the initiating POST that returns the job id charges that same flat one credit, and then, once a status poll reports the job completed, a second charge lands based on the cumulative creditsUsed Firecrawl itself reports at that point — the two are separate line items, not one deduplicated total. An in-progress poll never bills on its own, and polling a job that’s already completed a second time doesn’t bill again either, since that charge is keyed to the job id rather than the individual poll request. Either way it’s Firecrawl’s own metered rate with no NativePort markup; see the current per-credit price on the Firecrawl provider page. A zero balance answers every request with 402 rather than letting a crawl run partway and stall. Adding credit carries a flat 5.5% fee on top-ups between $10 and $5,000 — never on the calls themselves. Full breakdown: pricing.

Where to go next