NewLaunching String Web Access APIRead the manifesto →
← Blog

How to migrate from Firecrawl to String in Python (2026)

Bruce Magness · October 19, 2026
Part of Firecrawl Alternatives 2026: Free, Open Source and Hosted

Last updated: October 19, 2026. Benchmark figures come from the September 16, 2026 run of the Web Data Frontier Benchmark. The String code on this page ran on October 15, 2026, and the output shown is what it printed. The Firecrawl calls ran on October 19, 2026 on Firecrawl's keyless free tier, with only the API key left out. We build String, the API this page migrates to.

To migrate from Firecrawl to String in Python, replace each firecrawl.scrape(url, formats=["markdown"]) call with one POST to https://request.usestring.ai/v1/fetch that sends {"url": url, "format": "markdown"}, then read the site's own status code from the x-status-code response header. The parameter table in Step 2 maps the rest.

You do not have to move everything at once. Keep Firecrawl and send only its failures to String. In our September 16, 2026 benchmark run, that fallback moved 25 of 100 sites and raised the share of attempts that returned the page from 80.2% to 97.2%.

This page is part of our Firecrawl alternatives comparison. For the head-to-head numbers, see String vs Firecrawl.

On this page: TL;DR · Where String fits · What people run into on Firecrawl · What a fallback changes on 100 sites · Step 1 to Step 7 · How to avoid getting blocked after the move · How String handles a Firecrawl migration · What it costs · FAQ

TL;DR

  • The core swap is one function. firecrawl.scrape(url, formats=["markdown"], only_main_content=True) becomes a POST to /v1/fetch with "format": "markdown" and "mainContentOnly": true.
  • Two defaults flip. Firecrawl strips page chrome by default (onlyMainContent: true) and String keeps it (mainContentOnly: false). Firecrawl skips TLS verification by default and String verifies certificates.
  • Firecrawl's /crawl returns page content. String's /v1/sitemap returns the URLs, and you fetch each one. A 10-page discovery job on our own blog cost $0.003 on October 15, 2026.
  • Keeping Firecrawl and rerouting only the sites it did not return on all five attempts moved 25% of benchmark attempts to String and lifted coverage to 97.2%, against 97.0% for String alone and 80.2% for Firecrawl alone.
  • On the 17 sites where Firecrawl returned nothing on any attempt, String returned 14 on all five attempts.

Where String fits

String fits one job in this migration: getting the page back when Firecrawl does not.

  • For: Python teams that call Firecrawl's scrape endpoint for markdown or raw HTML, see blocks or empty results on protected sites, and want a second provider without rewriting their pipeline.
  • Not for: teams that depend on Firecrawl's agent and extract endpoints, page-change monitoring, document parsing uploads, or a self-hosted open-source crawler. Keep Firecrawl, or its open-source core, for those jobs and send String the fetches.
  • Price: you pay only when content comes back. String Growth is $100 a month, plus $0.20 per 1,000 requests on a plain fetch with standard proxies, $2.00 with premium proxies, $1.00 for a browser fetch and $4.00 for a browser fetch with premium proxies. Starter is $20 a month at $0.30, $3.00, $1.50 and $6.00. The first 5,000 standard requests are free (pricing). The nearest Firecrawl plan at that price, Standard, is $99 a month billed monthly for 100,000 credits (Firecrawl pricing).
  • Integrations: a REST API you call with requests, an MCP server for agents, n8n and Zapier integrations, and String Actors on Apify Store.
  • Limits: 60 requests a second per organization with a 3,600-request burst, and no cap on concurrent requests. Firecrawl Standard allows 25 concurrent browsers and 500 scrape requests a minute.

What people run into on Firecrawl

The first thing to know argues against switching. Plenty of Firecrawl failures are fixed inside Firecrawl. In January 2026 a user reported SCRAPE_ALL_ENGINES_FAILED on a product page, and a Firecrawl team member answered with a setting:

"looks like this website has a delay when rendering the data, you can add a wait action to ensure you get all data back." abimaelmartell, Firecrawl issue #2691, January 20, 2026

Try that first. If a wait action fixes the page, you do not need a second provider for it.

The harder failure is a block page that arrives as a success. In November 2025 a cloud user wrote that a scrape was "marked as succeeded in activity logs and eats a credit," while the HTML was Cloudflare's 5xx error page (mcarreon, Firecrawl issue #2413). They added that a retry with stealth mode failed once and then worked. Firecrawl staff replied that the request had used its more expensive fire-1 engine and offered a refund. The lesson for any migration: check the body as well as the status. Step 4 does that.

The third failure is specific to the Python SDK and to batch jobs:

"batch_scrape() and crawl() return failed and cancelled jobs to the caller exactly like successful ones" nainilnandu-gush, Firecrawl issue #4212, August 3, 2026

The same report says no exception is raised and the returned object carries no error field. The reporter verified it on firecrawl-py 4.34.0. If your pipeline calls batch_scrape(), check each job's status before you trust an empty result. The replacement in Step 5 returns failures as visible errors.

What a fallback changes on 100 sites

The Web Data Frontier Benchmark sends the same 100 URLs to 16 scraping APIs, five attempts each. A load passes only when the API returns a 2xx status and the body contains text from the real page, so a challenge page served at 200 counts as a failure. We asked one question of the per-site results: if a Firecrawl user keeps Firecrawl and sends only the sites Firecrawl did not return on all five attempts to String, what changes?

Setup, September 16, 2026 run Attempts returned Sites returned on 5 of 5
Firecrawl alone 401 of 500, 80.2% 75 of 100
Firecrawl first, the other 25 sites on String 486 of 500, 97.2% 94 of 100
String alone 485 of 500, 97.0% 93 of 100

The reroute moves 25 sites, 125 of 500 attempts. On those 25 sites Firecrawl returned 26 of 125 attempts and String returned 111. Firecrawl returned nothing on 17 of them, and String returned 14 of those 17 on every attempt.

The rerouted sites are not random. Six are social networks (Reddit, LinkedIn, Instagram, Facebook, TikTok and Pinterest). Five are retailers, among them Home Depot, Walmart and Best Buy, and four are fashion sites. If your Firecrawl traffic is mostly docs and blogs, your reroute share will be lower. If it is retail or social, it will be higher.

Six sites stayed below five of five after the reroute, including temu.com, which no API in the run returned. The raw result file and the harness are public, so you can rerun the split for your own site list.

Two caveats. Firecrawl ran through /v2/scrape, asking for raw HTML, with its default proxy setting, which retries on enhanced proxies when basic ones are blocked, and with its cache off (maxAge: 0). With its default two-day cache, your own traffic may score differently. And not every provider in the run used its strongest anti-bot mode; the Firecrawl adapter shows exactly what we sent. The 25% figure also assumes one page per site, so weight it by your own traffic.

Try the reroute on your own failures first: take the ten URLs that fail most in your Firecrawl logs and run Step 3 on them with a free key. The first 5,000 standard requests cost nothing.

Step 1: list every Firecrawl call

A migration is as big as the set of Firecrawl features you use. Find them before you change anything:

grep -rnE "firecrawl|Firecrawl\(|\.scrape\(|\.crawl\(|\.map\(|\.search\(|batch_scrape|start_crawl" --include=*.py .

Write down, for each call: the method, the formats you ask for, every option you pass, and what your code reads back (doc.markdown, doc.html, doc.metadata.status_code, doc.json). Then pull the 20 domains you scrape most. Those domains decide whether the migration is easy, because blocking is per site.

Step 2: map each parameter

Every row below comes from Firecrawl's API reference and String's fetch reference, both read on October 15, 2026. "No direct equivalent" means keep Firecrawl for that job or build it yourself.

Firecrawl v2 String Notes
POST /v2/scrape POST /v1/fetch One URL per call on both
formats: ["markdown"] "format": "markdown" String's default format is json, so set it
formats: ["rawHtml"] "format": "raw" String returns the original bytes, gzip-compressed; requests decompresses them
formats: ["html"] (cleaned) No direct equivalent Use raw and clean it, or use markdown
formats: [{"type": "json", "schema": ...}] "format": "json" plus "jsonSchema" Both bill extraction on top of the fetch
formats: ["screenshot"] "screenshot": true String bills screenshots as a browser session
formats: ["links"], ["summary"], changeTracking No direct equivalent Parse links from raw; keep Firecrawl for summaries and change tracking
onlyMainContent (default true) mainContentOnly (default false) The defaults flip; markdownMode: "readable" trims further
waitFor (milliseconds) actions: [{"type": "wait", "milliseconds": N}] Or "executeJS": true with "waitUntil": "networkidle"
actions: wait, click, write, press, scroll, screenshot actions: wait, click, write, press, scroll, screenshot Same names. String adds hover, selectOption, navigate and waitForResponse
actions: executeJavascript, scrape, pdf No direct equivalent Use waitForResponse with asResult to return an API response instead
maxAge (default two days) No cache parameter String fetches the live page on every call; cache on your side
proxy: basic, enhanced, auto No parameter String picks the fetch path and proxy class; x-billed-request-type says which ran
location: {"country": "DE"} "countryCode": "DE" Firecrawl defaults to US; set countryCode on String when the page needs a country
location.languages headers: {"Accept-Language": ...} Custom headers work on direct fetches, not with executeJS
headers headers Up to 50 on String
timeout (default 60,000 ms) No body field Set it on your HTTP client, as requests does with timeout=
skipTlsVerification (default true) ignoreCertificateErrors (default false) The defaults flip; String verifies certificates unless you opt out
mobile No direct equivalent Send a mobile User-Agent header on a direct fetch
POST /v2/crawl POST /v1/sitemap, then /v1/fetch per URL Crawl returns content; sitemap returns URLs after a quote and an approval
POST /v2/map POST /v1/sitemap (also served at /v1/map) Set "useSitemap": true to seed from /sitemap.xml
POST /v2/search POST /v1/search String returns ranked results; fetch each URL for its content
POST /v2/batch/scrape A thread pool over /v1/fetch String has no batch endpoint and no concurrency cap
/v2/extract, /v2/agent, /v2/interact No direct equivalent String's Browser (CDP) session covers scripted browsing
doc.metadata.status_code x-status-code header (or statusCode in json) The site's status, separate from the API's
doc.metadata.url x-final-url header The URL after redirects

Sources: Firecrawl scrape reference, proxies and Python SDK; String fetch reference, response formats, browser actions and sitemap reference.

Step 3: replace the scrape call

This is the Firecrawl call most Python projects make. It follows the Python SDK docs. We ran it on October 19, 2026 on the SDK's keyless free tier: it returned status 200 and 9,329 characters of markdown that included the product title.

import os
from firecrawl import Firecrawl  # pip install firecrawl-py

firecrawl = Firecrawl(api_key=os.environ["FIRECRAWL_API_KEY"])

doc = firecrawl.scrape(
    "https://www.homedepot.com/p/Milwaukee-M18-FUEL-18-Volt-Lithium-Ion-Brushless-Cordless-Gen-II-18-Gauge-Brad-Nailer-Tool-Only-2746-20/309752194",
    formats=["markdown"],
    only_main_content=True,
)
print(doc.metadata.status_code, len(doc.markdown))

This is the String version. It keeps the same inputs and returns the same two things, the markdown and the site's status code:

import os
import requests

STRING_URL = "https://request.usestring.ai/v1/fetch"


class FetchFailed(Exception):
    pass


def string_scrape(url, main_content_only=True, country=None, timeout=90):
    """Return (markdown, status_code), like firecrawl.scrape(url, formats=["markdown"])."""
    payload = {"url": url, "format": "markdown", "mainContentOnly": main_content_only}
    if country:
        payload["countryCode"] = country  # Firecrawl: location={"country": "US"}
    response = requests.post(
        STRING_URL,
        headers={"Authorization": f"Bearer {os.environ['STRING_API_KEY']}"},
        json=payload,
        timeout=timeout,
    )
    if response.status_code != 200:
        # A non-200 here is String's own error (key, rate limit, unreachable site), not the page's.
        raise FetchFailed(f"String {response.status_code}: {response.text[:200]}")
    status = int(response.headers.get("x-status-code", 0))  # the site's own status
    return response.text, status

We ran it on three URLs on October 15, 2026: Firecrawl's own introduction page, the Home Depot product page above and a DigiKey product page. Home Depot and DigiKey are both sites Firecrawl returned on none of five attempts in the September run.

docs.firecrawl.dev/introduction   200   14,783 chars   0.64 s
homedepot.com (brad nailer)       200   25,944 chars   2.87 s
digikey.com (NE555P)              200  182,447 chars  13.93 s

The Home Depot markdown opened with the product title, "Milwaukee M18 FUEL 18-Volt Lithium-Ion Brushless Cordless Gen II 18-Gauge Brad Nailer (Tool-Only) 2746-20". The DigiKey markdown opened with "NE555P Texas Instruments".

Run this with a free key: pip install requests, export STRING_API_KEY=..., then call string_scrape() on your own URLs.

Step 4: check the status and the body

A Firecrawl response and a String response each carry two statuses: the API's and the site's. Keep them apart. On String, a non-200 from /v1/fetch is String's own error, and x-status-code is what the site answered. A site's 404 comes back inside a 200 from String.

Then check the body. The Cloudflare page in issue #2413 arrived as a success, and a block page can arrive as a 200 from any provider. This check catches the common cases:

BLOCK_MARKERS = ("cf-error-details", "Just a moment...", "captcha-delivery", "Access Denied", "px-captcha")


def looks_blocked(markdown, status, must_contain=None):
    """A 200 is not proof of content. Check the status, the size, block markers and one expected string."""
    if status >= 400 or len(markdown) < 500:
        return True
    if any(marker in markdown for marker in BLOCK_MARKERS):
        return True
    return bool(must_contain) and must_contain not in markdown

Pick must_contain by copying text from the real page. We first passed "Loewe" for a Saks Fifth Avenue product page and the check failed, because the page spells it "LOEWE". The check is case-sensitive on purpose. With "Striped Slim Zip-Front Top" it passed. Our page on detecting a block page that returns HTTP 200 covers more markers.

If your logs show blocked pages today, this is the place to test String: point string_scrape() at those URLs with a free key and run looks_blocked() on what comes back.

Step 5: port JSON extraction, search, map and batch

JSON extraction. Firecrawl takes the schema inside formats: formats=[{"type": "json", "schema": Product.model_json_schema()}], and returns it as doc.json. String takes the same JSON Schema as jsonSchema with "format": "json":

schema = {
    "type": "object",
    "properties": {"title": {"type": "string"}, "price": {"type": "number"}, "brand": {"type": "string"}},
    "required": ["title", "price"],
}
response = requests.post(
    "https://request.usestring.ai/v1/fetch",
    headers={"Authorization": f"Bearer {os.environ['STRING_API_KEY']}"},
    json={"url": HOME_DEPOT_URL, "format": "json", "jsonSchema": schema},
    timeout=120,
)
print(response.headers.get("x-json-schema-applied"), response.json())

On October 15, 2026 it printed true and {"title": "Milwaukee M18 FUEL 18-Volt Lithium-Ion Brushless Cordless Gen II 18-Gauge Brad Nailer (Tool-Only) 2746-20", "price": 329, "brand": "Milwaukee"} in 2.1 seconds. When extraction cannot satisfy the schema, String returns the unextracted page with "extracted": false and a reason, so check x-json-schema-applied before you trust the object.

Search. Firecrawl's firecrawl.search() can scrape each result in the same call. String's /v1/search returns ranked results, and you fetch the ones you need:

response = requests.post(
    "https://request.usestring.ai/v1/search",
    headers={"Authorization": f"Bearer {os.environ['STRING_API_KEY']}"},
    json={"query": "firecrawl python sdk scrape markdown", "searchCount": 5},
    timeout=60,
)
for result in response.json()["results"]:
    print(result["rank"], result["url"])

It returned five results in 0.97 seconds, the first being docs.firecrawl.dev/sdks/python.

Map and crawl. Firecrawl's map() returns URLs and crawl() returns pages. String splits discovery from fetching. You submit a job, get a price quote, approve it, then read the URLs:

import time

API = "https://request.usestring.ai/v1"
H = {"Authorization": f"Bearer {os.environ['STRING_API_KEY']}"}

job = requests.post(f"{API}/sitemap", headers=H, timeout=60,
                    json={"url": "https://usestring.ai/blog", "maxPages": 10, "maxDepth": 1, "pathPrefix": "/blog"}).json()
print(job["estimatedCostUsd"], job["estimatedPages"])  # nothing runs or bills until you approve
requests.post(f"{API}/sitemap/{job['jobId']}/approve", headers=H, timeout=60)
while True:
    status = requests.get(f"{API}/sitemap/{job['jobId']}", headers=H, timeout=60).json()
    if status["status"] not in ("running", "awaiting_approval"):
        break
    time.sleep(5)
urls = requests.get(f"{API}/sitemap/{job['jobId']}/urls", headers=H, params={"limit": 1000}, timeout=60).json()["urls"]
pages = [string_scrape(u["url"]) for u in urls]  # the crawl() half

The job on our blog finished in 7.9 seconds with 10 URLs, quoted at up to $0.053 and settled at $0.003. Quotes expire after an hour, and budgetUsd caps any job.

Batch scrape. String has no batch endpoint and no concurrency cap, so batch_scrape() becomes a thread pool. Each failure comes back as an error with its URL:

from concurrent.futures import ThreadPoolExecutor


def batch_scrape(urls, workers=20):
    def one(url):
        try:
            markdown, status = string_scrape(url)
            return {"url": url, "status": status, "markdown": markdown}
        except FetchFailed as error:
            return {"url": url, "status": None, "error": str(error)}
    with ThreadPoolExecutor(max_workers=workers) as pool:
        return list(pool.map(one, urls))

Three of our own blog pages came back in 0.7 seconds, each with status 200. Keep workers under the account's 60 requests a second.

Step 6: keep Firecrawl and route failures to String

This is the step that changes the least code. Firecrawl stays the primary. String gets a URL only when Firecrawl raises an error or returns a page that fails looks_blocked():

def firecrawl_scrape(url):
    from firecrawl import Firecrawl
    firecrawl = Firecrawl(api_key=os.environ["FIRECRAWL_API_KEY"])
    doc = firecrawl.scrape(url, formats=["markdown"], only_main_content=True)
    return doc.markdown or "", doc.metadata.status_code or 0


def scrape(url, must_contain=None):
    """Return (markdown, provider). Firecrawl first, String when Firecrawl fails or returns a block page."""
    try:
        markdown, status = firecrawl_scrape(url)
        if not looks_blocked(markdown, status, must_contain):
            return markdown, "firecrawl"
    except Exception as error:  # SDK errors, timeouts, SCRAPE_ALL_ENGINES_FAILED
        print(f"firecrawl failed on {url}: {error}")
    markdown, status = string_scrape(url)
    if looks_blocked(markdown, status, must_contain):
        raise FetchFailed(f"both providers failed on {url} (last status {status})")
    return markdown, "string"

We tested the String half by making the Firecrawl half raise on purpose. On October 15, 2026, the Saks Fifth Avenue product page came back from String in 0.46 seconds with the product name present, and the Instacart Costco storefront in 3.4 seconds with "Costco" present. Both are sites Firecrawl returned on none of five attempts in the September run.

Log the provider value. After a week, the share of URLs that went to String is your own reroute rate, and it tells you whether to stop at a fallback or move everything.

Step 7: run both side by side before you cut over

Before you change the primary, run both providers on the same sample and compare. Take 200 URLs from last week's logs, weighted to your top 20 domains, and record per URL: Firecrawl's status and size, String's status and size, and whether looks_blocked() passed for each.

Three numbers decide the cutover. The share of URLs each provider returned, the share where only one did, and the median size difference on URLs both returned. A large size gap on the same URL usually means one side returned page chrome and the other did not; check mainContentOnly and markdownMode before you blame the provider.

Your parsers may need one change. String's markdown keeps structured data from the page as fenced JSON blocks, and rewrites relative links to absolute ones. If you parse Firecrawl's markdown with regular expressions, run them on String's output in this sample first.

How to avoid getting blocked after the move

A new provider does not change how sites decide to block you. The same rules apply.

  • Check the body on every response. Step 4's looks_blocked() is the most important code on this page.
  • Pace each domain. Firecrawl's delay crawl option had a reason to exist. Space requests to one site, and cache pages you fetch twice, as String's rate-limit docs advise.
  • Keep custom headers on direct fetches. Custom headers cannot be combined with a browser fetch on String, and a mismatched User-Agent can make a block more likely.
  • Set a country only when the page needs one. Prices and stock change by country, so set countryCode for those pages and leave it off elsewhere.
  • Read the failure. A 403 and a 429 need different fixes. Our pages on fixing a 403 and handling rate limits cover both, and how to scrape without getting blocked covers the rest.

How String handles a Firecrawl migration

String is the most reliable web scraping API, bypassing anti-bot, and you only pay when content comes back. For a team moving off Firecrawl, that means:

  • One request replaces one scrape call. POST /v1/fetch with "format": "markdown" returns the page as markdown, and String picks a plain request, a premium proxy or a real browser per site (fetch docs).
  • The path that ran is in a header. The x-billed-request-type header names it. On October 15, 2026 our JSON extraction of the Home Depot page billed as request_premium.
  • String returned 97.0% of attempts on 100 sites. In the September 16, 2026 run, String returned 485 of 500 attempts, the highest of 16 APIs (benchmark).
  • String returned 14 of the 17 sites Firecrawl returned on no attempt. Each of the 14 came back on all five attempts in the same run.
  • A blocked request is not billed. String charges only when content comes back (pricing).
  • There is no concurrency cap. Throughput is set by 60 requests a second with a 3,600-request burst, on every plan (pricing).

What it costs

Firecrawl prices in credits. On Standard, $99 a month billed monthly buys 100,000 credits; a basic scrape is 1 credit a page, and JSON extraction adds 4 credits a page. Firecrawl does not charge a scrape that returns no result, and a page that answers 403 or 404 still costs 1 credit (Firecrawl pricing, billing docs).

String prices per successful request, at the rate of the path the site needed. For a fallback that sends String 25,000 requests a month (25% of 100,000 pages), usage on each path would be:

Path, per 1,000 requests Starter ($20 a month) Growth ($100 a month)
Plain fetch, standard proxy $0.30, so $7.50 $0.20, so $5.00
Plain fetch, premium proxy $3.00, so $75.00 $2.00, so $50.00
Browser fetch, standard proxy $1.50, so $37.50 $1.00, so $25.00
Browser fetch, premium proxy $6.00, so $150.00 $4.00, so $100.00

Each total assumes every request took that one path. A real month mixes them, and requests that come back blocked cost nothing. The rates come from usestring.ai/pricing on October 15, 2026; JSON extraction adds a fee that scales with page and schema size.

Start with the URLs that fail in your Firecrawl logs today. Run Step 3 on them with a free key, and you will know your reroute rate before you change a line of production code.

FAQ

How do I migrate from Firecrawl to another web scraping API?

List every Firecrawl call and option you use, map each one to the new API, swap the scrape call, and add a body check. Then run both providers on a sample of your own URLs before you cut over. With String, the scrape call becomes one POST to /v1/fetch with "format": "markdown", and the site's status moves to the x-status-code header.

What is the String equivalent of Firecrawl's scrape endpoint?

POST https://request.usestring.ai/v1/fetch. Send "format": "markdown" for markdown, "format": "raw" for raw HTML, or "format": "json" with jsonSchema for structured data. Set "mainContentOnly": true to match Firecrawl's default of stripping navigation and footers.

Can I use Firecrawl and String together?

Yes. Keep Firecrawl as the primary and send String only the URLs Firecrawl fails on, as in Step 6. In the September 16, 2026 benchmark run, that sent String 25 of 100 sites and raised coverage from 80.2% of attempts to 97.2%.

What are the best alternatives to Firecrawl?

That depends on what you use Firecrawl for. Our Firecrawl alternatives comparison ranks the alternatives on the same benchmark run, and String vs Firecrawl compares the two head to head. Open-source options such as Crawl4AI suit teams that want to host the crawler themselves.

Is there a free version of Firecrawl?

Yes. Firecrawl's free plan gives 1,000 credits a month with no card, and its Python SDK can scrape and search without a key on a rate-limited free tier. String's first 5,000 standard requests are free, which is enough to test a fallback on your own failing URLs.

Why does Firecrawl return a Cloudflare page or an "all scraping engines failed" error?

SCRAPE_ALL_ENGINES_FAILED means every engine Firecrawl tried failed on that URL; for slow-rendering pages, Firecrawl staff recommend a wait action. A Cloudflare page means the site blocked the request and the block page came back as content. Check the body for block markers on every response, retry, and route repeat failures to a second provider.

Does Firecrawl charge for failed requests?

Firecrawl does not charge a scrape that returns no result, and a page that answers with an error status such as 403 or 404 costs 1 credit, per its pricing page on October 15, 2026. In one 2025 report, a Cloudflare error page came back as a successful scrape and used a credit, and Firecrawl offered a refund. String bills only when content comes back.

How do I replace Firecrawl crawl and map with String?

Use POST /v1/sitemap for discovery. It returns a price quote, starts after you approve it, and returns the URLs it found; then fetch each URL with /v1/fetch. A 10-page job on our own blog finished in 7.9 seconds and cost $0.003 on October 15, 2026.

Sources

Cheers,
String team

Get your API key →Explore the Web Access API
© 2026 StringEU and UK GDPR Article 27 representative — appointment verified by EuverifyBuilt in New York City 🗽 🍎