Documentation menu
HTTP scan API
Run a passive Settlens scan from your own backend with one authenticated POST request and get the JSON report back.
In short
POST /api/scan on settlens.com runs the same passive scan as settlens scan on one public URL, with an optional facilitator, and returns the JSON report. It needs a bearer token issued to pilots, allows 30 requests per minute per client IP, refuses private destinations and never runs the active probe.
On this page
The Settlens website exposes one scan endpoint for server-to-server use. It runs the same passive scan as settlens scan on a public URL and returns the JSON report, with its integrity digest, in the response body. It never sends a payment signature, never runs the active probe and never calls /settle.
Looking for the agent interface?
AI agents should use the MCP server instead: its remote endpoint at https://settlens.com/mcp offers the offline verification tools publicly and the passive network tools with the same token as this API.
Authentication
Every request needs Authorization: Bearer <token>; the Bearer scheme is required. The token is compared in constant time with the one configured on the server; a missing or wrong token gets 401. A server with no token, a placeholder token or one shorter than 32 characters disables the API and answers 401 to everyone. Tokens are not self-service: ask for one with your pilot request. Call the API from a backend only: it sends no CORS headers, and a token in browser code is a token anyone can read.
Request
POST /api/scan HTTP/1.1
Host: settlens.com
Authorization: Bearer <your API token>
Content-Type: application/json
{ "url": "https://api.merchant.example/v1/weather", "facilitator": "https://facilitator.example" }
| Field | Required | Description |
|---|---|---|
| url | Yes | The paid resource to scan: an absolute http or https URL of at most 2048 characters that passes the destination rules below. |
| facilitator | No | Base URL of a facilitator to cross-check with GET /supported: a string of at most 2048 characters (an object is refused) that passes the same destination rules. |
The scan is fixed: GET, no custom headers, two samples, an 8000 ms timeout per request and no local policy, so policy checks do not run. To scan with a policy, custom headers or the authorized active probe, use the CLI in your own environment.
curl -sS https://settlens.com/api/scan \
-H "Authorization: Bearer $SETTLENS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"url":"https://api.merchant.example/v1/weather"}' \
-o report.json
Response
A successful call answers 200 with the complete report: the same schema as the CLI's JSON report, with metadata.source set to settlens-web-api. Findings are part of the report, not HTTP errors: an endpoint that is not an x402 resource still gets 200 and a report with failing checks. Excerpt of a real call against a public page that does not ask for payment:
HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
cache-control: no-store
{
"schemaVersion": 1,
"reportId": "a01d0965",
"product": "Settlens",
"metadata": { "source": "settlens-web-api" },
"scope": { "targetCount": 1, "activeTargetCount": 0, "settlementCalls": 0, "privateKeysUsed": 0 },
"summary": { "pass": 3, "fail": 2, "warn": 0, "skip": 1 },
"targets": [
{
"mode": "passive",
"checks": [
{ "id": "transport.status_402", "status": "fail", "severity": "high",
"message": "Expected HTTP 402 but received HTTP 200.", "…": "…" }
],
"…": "…"
}
],
"integrity": {
"algorithm": "sha256",
"canonicalization": "settlens-stable-json-v2",
"digest": "eb165ca0664092eee3f321d0b4643de8b4df02eae3f4a143033d09c315645bad"
}
}
Save the body as a file and it verifies like any other report: settlens verify-report report.json. Field reference: JSON report.
Errors
Errors are JSON objects with a single error message.
| Status | When | Example body |
|---|---|---|
| 400 | Body is not a JSON object, url is missing or too long, facilitator is not a URL string of at most 2048 characters, or a destination is refused | {"error":"Private or non-routable destinations are not allowed."} |
| 401 | Missing or wrong bearer token | {"error":"Authentication required."} |
| 405 | Any method other than POST (with Allow: POST) | {"error":"Method not allowed."} |
| 413 | Body larger than 32 KiB | {"error":"Request body is too large."} |
| 429 | More than 30 requests in 60 seconds from one client IP, failed authentications included | {"error":"Rate limit exceeded."} |
| 500 | Unexpected server error | {"error":"Internal server error."} |
Other destination messages you may see: Only http and https destinations are allowed., Only ports 80 and 443 are allowed., Credentials in the URL are not allowed., Local destinations are not allowed., The destination resolves to a private or non-routable address. and Could not resolve <host>: ….
Limits
- 30 requests per client IP per 60-second window. Requests rejected with
401count too. - Request bodies up to 32 KiB;
urlup to 2048 characters. - Each scan makes up to two requests to the target (one when the first answer has no
PAYMENT-REQUIRED), plus one to/supportedwhen a facilitator is given, each with an 8000 ms timeout and a 1 MiB cap on the body read.
Destination rules (SSRF protection)
The API makes requests from Settlens' server, so it only reaches public destinations. Before any request, the target and facilitator URLs are checked:
- Only
httpandhttps, on ports 80 and 443 only, with no credentials in the URL. - No
localhost, and no host name ending in.localor.internal. - An IP literal, and every address a host name resolves to, must be publicly routable. Refused IPv4 ranges include private networks (10/8, 172.16/12, 192.168/16), loopback (127/8), link-local (169.254/16, which covers cloud metadata services), shared CGNAT space (100.64/10), documentation, benchmarking, multicast and reserved ranges. For IPv6 only global unicast is accepted, without documentation and Teredo prefixes, and an IPv4 address embedded in IPv6 (mapped, NAT64 or 6to4) must itself be public.
- Redirects are never followed, so a public URL cannot bounce the request to an internal one.
The HTTP client resolves the host again when it connects, after the check. That gap is accepted because the API is authenticated, passive and follows no redirects.
What is not an API
/api/pilot receives the website's pilot request form. It is not a public API and has no stability guarantees; use the contact page to reach the team.
Questions
Can the API run the active probe?
No. The API only runs passive scans. The authorized active probe is available only in the CLI, where you run it yourself with --active --authorized --authorization-ref.
Can I scan an endpoint on my private network?
Not through the API: private, loopback, link-local and other non-public destinations are refused. Run the CLI or the GitHub Action inside your network instead.
Does the API store the reports?
The endpoint returns the report in the response and does not write it anywhere. Keep the response body if you need it as evidence.
Sources
Last updated