Quick start
- Create a key. Sign in to the portal and create an API key on the Security page. It is shown once, on creation.
-
Run a report on a verified domain you own:
POST /v1/analysis/run. The response carries areport_id. -
Poll
GET /v1/analysis/report/{id}until itsstatusisready. - Fetch the findings, or the PDF.
# 1. The key from the portal Security page
export SEEKINGDATA_KEY="paste-your-key-here"
# 2. Run a report; note the report_id in the response
curl -sS -X POST \
"https://api.seekingdatalabs.com/v1/analysis/run?domain=example.com&report_type=analyzer" \
-H "Authorization: Bearer $SEEKINGDATA_KEY"
# 3. Poll until "status" is "ready"
curl -sS "https://api.seekingdatalabs.com/v1/analysis/report/rep-0a1b2c3d4e5f" \
-H "Authorization: Bearer $SEEKINGDATA_KEY"
# 4. Fetch the findings, then the PDF
curl -sS "https://api.seekingdatalabs.com/v1/analysis/report/rep-0a1b2c3d4e5f/findings" \
-H "Authorization: Bearer $SEEKINGDATA_KEY"
curl -sS "https://api.seekingdatalabs.com/v1/analysis/report/rep-0a1b2c3d4e5f/pdf" \
-H "Authorization: Bearer $SEEKINGDATA_KEY" -o report.pdf
Access is covered under Access.
Authentication
Sign in to the portal at /app/login/ and create an API key on the Security page; the plaintext key is shown once, on creation, and never again. Sightline is invite-only: to ask for an account, write to hello@seekingdatalabs.com.
Every request is authenticated one of two ways: the browser session
cookie (for calls made from the signed-in portal), or a
Bearer API key in the Authorization header
(for calls made by your own code or an agent). The examples below use
a Bearer key.
The reporting flow
A report goes through three steps: commission a run, poll it until it
is ready, then fetch the result. Runs are asynchronous: the
run call returns immediately with a report id and a
status, and the report fills in as the fleet works.
1. Run a report
POST /v1/analysis/run commissions a run. It requires a
verified domain you own and an account that covers the report
(see Access).
The report_type query
parameter selects the product: analyzer for the website
audit (the default) or asvs for the OWASP ASVS security
posture report. Any other value folds to analyzer.
curl -sS -X POST \
"https://api.seekingdatalabs.com/v1/analysis/run?domain=example.com&report_type=analyzer" \
-H "Authorization: Bearer $SEEKINGDATA_KEY"
The response carries a report_id and a
status.
2. Poll until ready
GET /v1/analysis/reports lists your runs. Fetch a single
run with GET /v1/analysis/report/{id} and poll it
until its status is ready.
The listing answers with the current report for each domain. Once a
newer report replaces an older one, the older is marked superseded
and left out of the list; pass ?archived=1 to get the
whole history back, with an archived_at on every
superseded run. Superseded reports keep resolving by id and by share
link, so anything you have already handed out still opens.
curl -sS "https://api.seekingdatalabs.com/v1/analysis/reports" \
-H "Authorization: Bearer $SEEKINGDATA_KEY"
curl -sS "https://api.seekingdatalabs.com/v1/analysis/report/rep-0a1b2c3d4e5f" \
-H "Authorization: Bearer $SEEKINGDATA_KEY"
3. Fetch the report
Once a run is ready, the report is available in these forms:
-
GET /v1/analysis/report/{id}, the full report envelope. -
GET /v1/analysis/report/{id}/findings, the structured findings for the run. Each finding carriessdl_id, the SDL-ID the PDF, the hosted report and the worklist print beside it (for example SDL-07), so a finding can be matched to its spreadsheet row. It is null for a security assessment, when the stored report cannot be read, or when the report prints no single number for the finding; it is never guessed. SDL-IDs are renumbered every report: to follow a finding across reports, use itssignature. -
GET /v1/analysis/report/{id}/pdf, the report as a PDF. -
GET /v1/analysis/report/{id}/worklist.csv, the report's worklist as a spreadsheet file, one row per page and issue. -
GET /v1/analysis/report/{id}/worklist.csv?view=issues, the same worklist as one row per SDL-ID in fix order, with every URL the issue reaches in one cell. Its last column, Stable keys, lists the row view's stable key for each page the issue reaches, one per line, so status ticks can be matched to the next report's issue list even though SDL-IDs and priorities are renumbered every run. A key built from the model's wording (no engine check, no quoted page text) can still drift between runs, so an unmatched key means check it by hand, never fixed. -
GET /v1/analysis/report/{id}/worklist.csv?view=pages, the same worklist as one row per audited page, with the page's numbered to-do in fix order in one cell. -
GET /v1/analysis/report/{id}/worklist.xlsx, the same worklist as one Excel workbook: the page, issue and row views as sheets, plus an About sheet naming the domain, the report and its date. -
GET /v1/analysis/report/{id}/diff, the re-audit delta against the previous report as JSON. Every row carrieslatest_sdl_idandprior_sdl_id, the same values as the two SDL-ID columns of diff.csv (null where the file leaves the cell empty). Owner only. -
GET /v1/analysis/report/{id}/diff.csv, the re-audit delta against the previous report, one row per finding. Owner only; there is no shared variant. -
GET /v1/analysis/progress?domain={domain}, the open-issue trend across every report of a domain, oldest first: open counts by severity, and fixed, new, still open, regressed and not re-checked against each report's previous report. A finding whose page was not read again counts as not re-checked, never as fixed. A report with no countable findings shows null, never 0. Addformat=csvfor the same rows as a spreadsheet file. Owner only.
curl -sS "https://api.seekingdatalabs.com/v1/analysis/report/rep-0a1b2c3d4e5f/findings" \
-H "Authorization: Bearer $SEEKINGDATA_KEY"
curl -sS "https://api.seekingdatalabs.com/v1/analysis/report/rep-0a1b2c3d4e5f/pdf" \
-H "Authorization: Bearer $SEEKINGDATA_KEY" \
-o report.pdf
Spreadsheet exports
The worklist and the re-audit delta download as CSV files a team can work through in a spreadsheet. They are built from the same report body the hosted page and the PDF read, so they add no claim the report does not make.
- Each file starts with a UTF-8 byte order mark, so a spreadsheet opens umlauts correctly.
-
The delimiter follows the language:
;for a German export,,otherwise. - A cell a spreadsheet would read as a formula is prefixed with an apostrophe.
- Only the website analysis has a worklist: a security assessment report answers 404, never an empty file.
- The worklist's status column is left empty for the team to fill in.
-
The worklist also downloads as one Excel workbook,
worklist.xlsx, with no byte order mark or delimiter to depend on. Its sheets are named in the report's language (Pages, Issues, All rows and About; in German Seiten, Probleme, Alle Zeilen and Info), and every cell is text, never a formula.
curl -sS "https://api.seekingdatalabs.com/v1/analysis/report/rep-0a1b2c3d4e5f/worklist.csv" \
-H "Authorization: Bearer $SEEKINGDATA_KEY" \
-o worklist.csv
Sharing a report
POST /v1/analysis/report/{id}/share mints an
unguessable share token; the matching
DELETE revokes it. Anyone holding the token can then read
the report with GET /v1/analysis/shared/{token},
which is public and needs no authentication. The token is the whole
credential, so share it deliberately. The same token also reads the
worklist with
GET /v1/analysis/shared/{token}/worklist.csv
(and ?view=issues or ?view=pages), and
the workbook with
GET /v1/analysis/shared/{token}/worklist.xlsx.
curl -sS -X POST \
"https://api.seekingdatalabs.com/v1/analysis/report/rep-0a1b2c3d4e5f/share" \
-H "Authorization: Bearer $SEEKINGDATA_KEY"
# The returned token reads publicly, no key:
curl -sS "https://api.seekingdatalabs.com/v1/analysis/shared/$SHARE_TOKEN"
Response shapes
A successful JSON read answers a plain object with the fields its route documents, and a list route carries its paging fields beside the rows. This is the findings list of one report:
HTTP/1.1 200 OK
Content-Type: application/json
{
"findings": [
{
"run_id": "rep-0a1b2c3d4e5f",
"signature": "homepage.accessibility.1cd918b02c5e",
"domain": "example.com",
"surface": "Homepage",
"bucket": "bugs",
"severity": "medium",
"kind": "accessibility",
"category": null,
"systemic": false,
"state": "open",
"detail": "[finding title]",
"evidence_url": "https://example.com/",
"generated_at": "2026-09-14T10:00:00.000Z",
"sdl_id": "SDL-07"
}
],
"run_id": "rep-0a1b2c3d4e5f",
"total": 1,
"limit": 50,
"offset": 0
}
Errors leave the success shape and follow RFC 9457. Check
retryable before trying again, and wait
retry_after seconds when it is present.
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 30
{
"type": "https://seekingdatalabs.com/errors/rate-limit",
"title": "Rate Limit Exceeded",
"status": 429,
"detail": "[what happened, one sentence]",
"instance": "about:blank",
"retryable": true,
"retry_after": 30,
"error_category": "rate_limit"
}
Access
Sightline is invite-only. A run your account does not
cover is held awaiting_payment until it is settled, and
queued once it is.
Account balance and transactions
GET /v1/credits returns your account balance and the
recent movements that built it. The balance is the running sum of an
append-only ledger, so the numbers always reconcile. Key fields:
-
balance_atomic, the running total over the whole ledger, in atomic USDC units (millionths).balance_usdis the same figure in dollars. -
entries[], the recent movements, each with anid,created_at, a signeddelta_atomic, adirection(creditordebit), areason, aref, and anactor. -
next_before, the pagination cursor. Pass it back asbefore=to fetch the next page; it isnullon the last page. -
topup, how to pay in: a deposit address and the exacttop-<account-id>memo that matches a transfer to your account (USDC on Solana). The memo is served complete; send it exactly, or the deposit cannot be matched to your account.
curl -sS "https://api.seekingdatalabs.com/v1/credits?limit=25" \
-H "Authorization: Bearer $SEEKINGDATA_KEY"
# Next page: pass the previous response's next_before
curl -sS "https://api.seekingdatalabs.com/v1/credits?limit=25&before=1024" \
-H "Authorization: Bearer $SEEKINGDATA_KEY"
A report your plan covers costs nothing extra. A transfer lands once it confirms on chain, not the instant it is sent.
Security assessments and consultancy are not self-serve
There is no API that runs a security assessment (a penetration test only with written authorisation for active testing). A security assessment is a scoped, authorized engagement: it runs only after an offline authorization and contract, and is dispatched internally by an operator. Nothing you can call here starts one.
What you can do through the API is record intent. A
POST /v1/requests files a request for a human operator to
review; it changes nothing about your account and starts no work. The
body takes a kind of
pentest, pentest_discussion, or
consultancy, a domain (required for the
pentest kinds; omitted for consultancy), and an optional free-text
detail. The pentest kind names the
contracted security assessment; the same request covers the
vulnerability scan and, with authorisation, the penetration test.
curl -sS -X POST https://api.seekingdatalabs.com/v1/requests \
-H "Authorization: Bearer $SEEKINGDATA_KEY" \
-H "Content-Type: application/json" \
-d '{"kind":"pentest","domain":"example.com","detail":"Pre-launch review of the checkout flow."}'
A repeat request for the same domain and kind stacks rather than duplicating. An operator picks it up from there.