{
"openapi": "3.0.3",
"info": {
"title": "CRW REST API",
"version": "0.16.0",
"description": "Firecrawl-compatible web scraping, crawling, mapping, search, and extraction API. Hosted at https://api.fastcrw.com; self-host with `docker run -p 3000:3000 ghcr.io/us/crw:latest`.",
"license": {
"name": "AGPL-3.0",
"url": "https://opensource.org/licenses/AGPL-3.0"
},
"contact": {
"url": "https://github.com/us/crw"
}
},
"servers": [
{
"url": "https://api.fastcrw.com",
"description": "Hosted (requires Bearer token)"
},
{
"url": "http://localhost:3000",
"description": "Self-hosted (no auth by default)"
}
],
"security": [
{
"bearerAuth": []
},
{}
],
"tags": [
{
"name": "search",
"description": "Web search via SearXNG (self-hosted) or managed (hosted)"
},
{
"name": "scrape",
"description": "Single-URL content extraction"
},
{
"name": "crawl",
"description": "Async multi-page crawl jobs"
},
{
"name": "map",
"description": "URL discovery without full scraping"
},
{
"name": "extract",
"description": "Structured JSON extraction via scrape + jsonOptions"
},
{
"name": "change-tracking",
"description": "Stateless diff of a scrape against a caller-supplied previous snapshot (monitor primitive)"
}
],
"paths": {
"/v1/search": {
"post": {
"tags": [
"search"
],
"summary": "Search the web and optionally scrape results",
"operationId": "search",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SearchRequest"
},
"examples": {
"minimal": {
"summary": "Top 3 results, no scraping",
"value": {
"query": "renewable energy trends 2024",
"limit": 3
}
},
"with_scrape": {
"summary": "Scrape each result as markdown",
"value": {
"query": "rust async runtime",
"limit": 5,
"scrapeOptions": {
"formats": [
"markdown"
],
"onlyMainContent": true
}
}
}
}
}
}
},
"responses": {
"200": {
"description": "Search results",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SearchResponse"
}
}
}
},
"400": {
"$ref": "#/components/responses/BadRequest"
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"429": {
"$ref": "#/components/responses/RateLimited"
},
"503": {
"$ref": "#/components/responses/SearchDisabled"
}
}
}
},
"/v1/scrape": {
"post": {
"tags": [
"scrape"
],
"summary": "Scrape one URL",
"operationId": "scrape",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ScrapeRequest"
}
}
}
},
"responses": {
"200": {
"description": "Scrape result",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ScrapeResponse"
}
}
}
},
"400": {
"$ref": "#/components/responses/BadRequest"
},
"401": {
"$ref": "#/components/responses/Unauthorized"
}
}
}
},
"/v1/map": {
"post": {
"tags": [
"map"
],
"summary": "Discover URLs on a site",
"operationId": "map",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MapRequest"
}
}
}
},
"responses": {
"200": {
"description": "List of URLs",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MapResponse"
}
}
}
}
}
}
},
"/v1/crawl": {
"post": {
"tags": [
"crawl"
],
"summary": "Start an async crawl job",
"operationId": "crawlStart",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CrawlRequest"
}
}
}
},
"responses": {
"200": {
"description": "Crawl job accepted; poll with /v1/crawl/{id}",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CrawlAccepted"
}
}
}
}
}
}
},
"/v1/crawl/{id}": {
"get": {
"tags": [
"crawl"
],
"summary": "Get crawl job status and results",
"operationId": "crawlStatus",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "Crawl job id returned by POST /v1/crawl"
}
],
"responses": {
"200": {
"description": "Job status + collected pages",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CrawlStatus"
}
}
}
},
"404": {
"$ref": "#/components/responses/NotFound"
}
}
}
},
"/v1/change-tracking/diff": {
"post": {
"tags": [
"change-tracking"
],
"summary": "Diff current scrape content against a previous snapshot",
"description": "Stateless: opencore stores nothing \u2014 the caller supplies the previous snapshot. Accepts a single body or a batch array (presence of batch selects batch mode). The crawl-path workhorse for monitors.",
"operationId": "changeTrackingDiff",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ChangeTrackingDiffRequest"
}
}
}
},
"responses": {
"200": {
"description": "Diff result (object for single, array for batch) wrapped in the standard envelope",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"success",
"data"
],
"properties": {
"success": {
"type": "boolean"
},
"data": {
"oneOf": [
{
"$ref": "#/components/schemas/ChangeTrackingResult"
},
{
"type": "array",
"items": {
"$ref": "#/components/schemas/ChangeTrackingResult"
}
}
]
}
}
}
}
}
},
"400": {
"$ref": "#/components/responses/BadRequest"
},
"401": {
"$ref": "#/components/responses/Unauthorized"
}
}
}
},
"/v1/extract": {
"post": {
"tags": [
"extract"
],
"summary": "Start an async multi-URL structured extraction job",
"description": "Native extraction. Returns a per-URL array via GET /v1/extract/{id}. At least one of `prompt` or `schema` is required.",
"operationId": "extractStart",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExtractRequest"
}
}
}
},
"responses": {
"200": {
"description": "Extract job accepted; poll with /v1/extract/{id}",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExtractAccepted"
}
}
}
},
"400": {
"$ref": "#/components/responses/BadRequest"
}
}
}
},
"/v1/extract/{id}": {
"get": {
"tags": [
"extract"
],
"summary": "Get extract job status and per-URL results",
"operationId": "extractStatus",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "Extract job id returned by POST /v1/extract"
}
],
"responses": {
"200": {
"description": "Job status + per-URL extraction results",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExtractStatus"
}
}
}
},
"404": {
"$ref": "#/components/responses/NotFound"
}
}
}
}
},
"components": {
"securitySchemes": {
"bearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "API key",
"description": "Hosted only. Get a key at https://fastcrw.com/register."
}
},
"schemas": {
"SearchRequest": {
"type": "object",
"required": [
"query"
],
"properties": {
"query": {
"type": "string",
"minLength": 1,
"maxLength": 2000
},
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 20,
"default": 5
},
"lang": {
"type": "string",
"example": "en"
},
"tbs": {
"type": "string",
"enum": [
"qdr:h",
"qdr:d",
"qdr:w",
"qdr:m",
"qdr:y"
]
},
"sources": {
"type": "array",
"items": {
"type": "string",
"enum": [
"web",
"news",
"images"
]
}
},
"categories": {
"type": "array",
"items": {
"type": "string"
}
},
"scrapeOptions": {
"$ref": "#/components/schemas/ScrapeOptions"
}
}
},
"SearchResult": {
"type": "object",
"required": [
"url",
"title"
],
"properties": {
"url": {
"type": "string",
"format": "uri"
},
"title": {
"type": "string"
},
"snippet": {
"type": "string",
"description": "Result blurb (LLM-ready summary line)"
},
"description": {
"type": "string",
"description": "Alias of snippet (same value, kept for Firecrawl response-shape parity)"
},
"position": {
"type": "integer"
},
"score": {
"type": "number"
},
"category": {
"type": "string",
"description": "SearXNG result category (e.g. 'general')"
},
"markdown": {
"type": "string",
"description": "Present when scrapeOptions.formats includes 'markdown'"
}
}
},
"SearchResponse": {
"type": "object",
"required": [
"success",
"data"
],
"properties": {
"success": {
"type": "boolean"
},
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/SearchResult"
}
}
}
},
"ScrapeOptions": {
"type": "object",
"properties": {
"formats": {
"type": "array",
"items": {
"type": "string",
"enum": [
"markdown",
"html",
"rawHtml",
"links"
]
},
"default": [
"markdown"
]
},
"onlyMainContent": {
"type": "boolean",
"default": true
}
}
},
"ScrapeRequest": {
"type": "object",
"required": [
"url"
],
"properties": {
"url": {
"type": "string",
"format": "uri"
},
"formats": {
"type": "array",
"items": {
"type": "string",
"enum": [
"markdown",
"html",
"rawHtml",
"plainText",
"links",
"json",
"summary",
"changeTracking"
]
},
"default": [
"markdown"
]
},
"onlyMainContent": {
"type": "boolean",
"default": true
},
"jsonOptions": {
"$ref": "#/components/schemas/JsonOptions"
},
"changeTracking": {
"$ref": "#/components/schemas/ChangeTrackingOptions",
"description": "Activated when formats includes 'changeTracking'. The format entry is the plain string; options ride on this sibling field."
},
"goal": {
"type": "string",
"maxLength": 2048,
"description": "Plain-language monitor goal for the meaningful-change judge"
},
"judgeEnabled": {
"type": "boolean",
"description": "Run the LLM meaningful-change judge on a changed page (requires goal)"
}
}
},
"ScrapeResponse": {
"type": "object",
"required": [
"success",
"data"
],
"properties": {
"success": {
"type": "boolean"
},
"warning": {
"type": "string",
"description": "Top-level advisory (e.g. fallback render notice)"
},
"data": {
"type": "object",
"properties": {
"markdown": {
"type": "string"
},
"html": {
"type": "string"
},
"rawHtml": {
"type": "string"
},
"links": {
"type": "array",
"items": {
"type": "string",
"format": "uri"
}
},
"json": {
"type": "object",
"additionalProperties": true
},
"metadata": {
"$ref": "#/components/schemas/PageMetadata"
},
"warning": {
"type": "string"
},
"warnings": {
"type": "array",
"items": {
"type": "string"
}
},
"renderDecision": {
"type": "string",
"description": "Which renderer ran (e.g. 'http', 'browser')"
},
"creditCost": {
"type": "number"
}
}
}
}
},
"JsonOptions": {
"type": "object",
"properties": {
"schema": {
"type": "object",
"description": "JSON schema for the extracted shape",
"additionalProperties": true
},
"prompt": {
"type": "string"
},
"systemPrompt": {
"type": "string"
}
}
},
"MapRequest": {
"type": "object",
"required": [
"url"
],
"properties": {
"url": {
"type": "string",
"format": "uri"
},
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 5000,
"default": 100
},
"search": {
"type": "string",
"description": "Optional substring filter applied to discovered URLs"
}
}
},
"MapResponse": {
"type": "object",
"required": [
"success",
"data"
],
"properties": {
"success": {
"type": "boolean"
},
"data": {
"type": "object",
"required": [
"links"
],
"properties": {
"links": {
"type": "array",
"items": {
"type": "string",
"format": "uri"
}
},
"droppedActionCount": {
"type": "integer"
},
"strippedTrackingCount": {
"type": "integer"
}
}
}
}
},
"CrawlRequest": {
"type": "object",
"required": [
"url"
],
"properties": {
"url": {
"type": "string",
"format": "uri"
},
"maxPages": {
"type": "integer",
"minimum": 1,
"default": 100
},
"maxDepth": {
"type": "integer",
"minimum": 0,
"default": 2
},
"scrapeOptions": {
"$ref": "#/components/schemas/ScrapeOptions"
}
}
},
"CrawlAccepted": {
"type": "object",
"required": [
"success",
"id"
],
"properties": {
"success": {
"type": "boolean"
},
"id": {
"type": "string"
},
"url": {
"type": "string",
"format": "uri",
"description": "URL to poll with GET /v1/crawl/{id}"
}
}
},
"CrawlStatus": {
"type": "object",
"required": [
"status"
],
"properties": {
"status": {
"type": "string",
"enum": [
"scraping",
"completed",
"failed"
]
},
"completed": {
"type": "integer"
},
"total": {
"type": "integer"
},
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ScrapeResponse"
}
}
}
},
"ExtractRequest": {
"type": "object",
"required": [
"urls"
],
"description": "At least one of `prompt` or `schema` must be present.",
"properties": {
"urls": {
"type": "array",
"items": {
"type": "string",
"format": "uri"
},
"description": "URLs to extract from (1..crawler.max_extract_urls)."
},
"prompt": {
"type": "string",
"description": "Free-text extraction objective."
},
"schema": {
"type": "object",
"additionalProperties": true,
"description": "JSON Schema constraining the extracted output."
},
"llmApiKey": {
"type": "string",
"description": "BYOK LLM API key."
},
"llmProvider": {
"type": "string"
},
"llmModel": {
"type": "string"
},
"basis": {
"type": "boolean",
"description": "Per-field attribution (Phase 2b). Rejected with 400 while unsupported."
}
}
},
"ExtractResponse": {
"type": "object",
"required": [
"success",
"data"
],
"properties": {
"success": {
"type": "boolean"
},
"data": {
"type": "object",
"additionalProperties": true
}
}
},
"PageMetadata": {
"type": "object",
"additionalProperties": true,
"properties": {
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"sourceURL": {
"type": "string",
"format": "uri"
},
"statusCode": {
"type": "integer"
}
}
},
"ChangeTrackingOptions": {
"type": "object",
"properties": {
"modes": {
"type": "array",
"items": {
"type": "string",
"enum": [
"gitDiff",
"json"
]
},
"description": "Diff surfaces. gitDiff = markdown unified diff + AST; json = per-field diff; both = mixed."
},
"schema": {
"type": "object",
"additionalProperties": true,
"description": "JSON schema of tracked fields (json/mixed mode)"
},
"prompt": {
"type": "string"
},
"previous": {
"$ref": "#/components/schemas/ChangeTrackingSnapshot"
},
"tag": {
"type": "string",
"description": "Opaque caller tag echoed on the result"
},
"contentType": {
"type": "string",
"description": "MIME type; non-text content is hashed, not diffed"
}
}
},
"ChangeTrackingSnapshot": {
"type": "object",
"properties": {
"markdown": {
"type": "string",
"description": "Present for gitDiff/mixed mode"
},
"json": {
"type": "object",
"additionalProperties": true,
"description": "Present for json/mixed mode"
},
"contentHash": {
"type": "string",
"description": "Mode-aware hash; persist + supply on the next check"
},
"capturedAt": {
"type": "string",
"description": "Caller-stamped capture time, echoed untouched"
}
}
},
"ChangeDiff": {
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "Unified markdown diff (gitDiff/mixed)"
},
"json": {
"type": "object",
"additionalProperties": true,
"description": "Parse-diff AST (gitDiff-only) OR per-field path map {previous,current} (json/mixed)"
}
}
},
"ChangeJudgment": {
"type": "object",
"required": [
"meaningful",
"confidence",
"reason"
],
"properties": {
"meaningful": {
"type": "boolean"
},
"confidence": {
"type": "string",
"enum": [
"low",
"medium",
"high"
]
},
"reason": {
"type": "string"
},
"meaningfulChanges": {
"type": "array",
"items": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"added",
"removed",
"changed"
]
},
"before": {
"type": "string"
},
"after": {
"type": "string"
},
"reason": {
"type": "string"
}
}
}
}
}
},
"ChangeTrackingResult": {
"type": "object",
"required": [
"status",
"contentHash"
],
"properties": {
"status": {
"type": "string",
"enum": [
"same",
"changed"
],
"description": "Per-page status. Set-level new/removed are computed by the caller's reconciler."
},
"firstObservation": {
"type": "boolean",
"description": "True when no previous was supplied \u2014 caller maps to 'new'"
},
"contentHash": {
"type": "string"
},
"snapshot": {
"$ref": "#/components/schemas/ChangeTrackingSnapshot"
},
"diff": {
"$ref": "#/components/schemas/ChangeDiff"
},
"judgment": {
"$ref": "#/components/schemas/ChangeJudgment"
},
"tag": {
"type": "string"
},
"truncated": {
"type": "boolean"
}
}
},
"ChangeTrackingDiffItem": {
"type": "object",
"required": [
"current"
],
"properties": {
"url": {
"type": "string",
"format": "uri"
},
"current": {
"type": "object",
"properties": {
"markdown": {
"type": "string"
},
"json": {
"type": "object",
"additionalProperties": true
}
}
},
"previous": {
"$ref": "#/components/schemas/ChangeTrackingSnapshot"
},
"modes": {
"type": "array",
"items": {
"type": "string",
"enum": [
"gitDiff",
"json"
]
}
},
"schema": {
"type": "object",
"additionalProperties": true
},
"prompt": {
"type": "string"
},
"contentType": {
"type": "string"
},
"tag": {
"type": "string"
}
}
},
"ChangeTrackingDiffRequest": {
"type": "object",
"description": "Single body (provide 'current') OR batch (provide 'batch'). Top-level modes/schema/prompt/contentType act as shared defaults in batch mode.",
"properties": {
"batch": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ChangeTrackingDiffItem"
}
},
"current": {
"type": "object",
"properties": {
"markdown": {
"type": "string"
},
"json": {
"type": "object",
"additionalProperties": true
}
}
},
"previous": {
"$ref": "#/components/schemas/ChangeTrackingSnapshot"
},
"modes": {
"type": "array",
"items": {
"type": "string",
"enum": [
"gitDiff",
"json"
]
}
},
"schema": {
"type": "object",
"additionalProperties": true
},
"prompt": {
"type": "string"
},
"contentType": {
"type": "string"
}
}
},
"Error": {
"type": "object",
"required": [
"success",
"error"
],
"properties": {
"success": {
"type": "boolean",
"enum": [
false
]
},
"error": {
"type": "string",
"description": "Machine-readable error code (see docs/error-codes.md)"
},
"message": {
"type": "string"
}
}
},
"ExtractAccepted": {
"type": "object",
"required": [
"id",
"status",
"urls"
],
"properties": {
"id": {
"type": "string"
},
"status": {
"type": "string",
"enum": [
"processing"
]
},
"urls": {
"type": "integer",
"description": "Count of URLs enqueued for fetch (preflight-failed URLs appear in results, not here)."
}
}
},
"ExtractUrlResult": {
"type": "object",
"required": [
"url",
"status"
],
"properties": {
"url": {
"type": "string"
},
"status": {
"type": "string",
"enum": [
"completed",
"failed"
]
},
"data": {
"type": "object",
"additionalProperties": true,
"description": "Extracted JSON object (present when status is completed)."
},
"error": {
"type": "string"
},
"llmUsage": {
"type": "object",
"additionalProperties": true,
"description": "Per-URL LLM token usage (provider/model/input/output tokens)."
}
}
},
"ExtractStatus": {
"type": "object",
"required": [
"id",
"status",
"expiresAt",
"creditsUsed",
"tokensUsed"
],
"properties": {
"id": {
"type": "string"
},
"status": {
"type": "string",
"enum": [
"processing",
"completed",
"failed"
]
},
"results": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ExtractUrlResult"
},
"description": "Per-URL results in original request order."
},
"error": {
"type": "string",
"description": "Job-level error, set only when every URL failed."
},
"expiresAt": {
"type": "string",
"format": "date-time"
},
"creditsUsed": {
"type": "integer"
},
"tokensUsed": {
"type": "integer"
}
}
}
},
"responses": {
"BadRequest": {
"description": "Invalid request body",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"Unauthorized": {
"description": "Missing or invalid bearer token (hosted only)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"NotFound": {
"description": "Resource not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"RateLimited": {
"description": "Rate limit exceeded",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"SearchDisabled": {
"description": "Search is disabled in self-hosted config (error: search_disabled)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
}