Script your morning brief.
One read-only endpoint, GET /api/v1/findings, returns the signed-in tenant's findings fused across subdomains and exposed cloud buckets. Designed for cron jobs, Slack bots, and CI checks — not for replacing the human who reads it.
One read. Your tenant only.
GET/api/v1/findingsbetter-auth session — cookie or bearer tokenapplication/jsonEvery request is tenant-scoped to the signed-in user — the response only contains findings for domains you own. There is no global or admin scope; use the dashboard for cross-tenant work.
Keyset pagination + CSV filters.
?limitinteger 1–100Maximum number of items per page. Larger pages make pagination faster; 20 is a good default for cron jobs.
?limit=50
?cursoropaque tokenPass the nextCursor value from the previous response. Omit on the first page. Tokens are short-lived base64url; treat them as opaque.
?cursor=eyJsYXN0U2Vlbi…
?severitycsv enumSingle value or comma-separated. `?severity=critical` and `?severity=critical,high` both work. Pass nothing to see every severity.
?severity=critical,high
?statuscsv enumSingle value or comma-separated. Useful for paging only what is currently open, or only what you have already suppressed.
?status=open
?domainstringFilter findings to a single enrolled domain name. Cross-tenant values are rejected with 403.
?domain=acme.co
What you get back.
Every row in items carries the brief essentials: identifier, severity, status, the asset it lives on, a one-line evidence snippet, and a suggested next-step action you can drop straight into a ticket.
{
"items": [
{
"id": "ckxyz123…",
"kind": "subdomain",
"severity": "critical",
"status": "open",
"evidence": "Certificate Transparency log entry for staging.acme.co — current host status: LIVE.",
"nextAction": "Investigate ownership and put a 404/redirect in front, or add to your allow list.",
"lastSeenAt": "2026-04-12T10:14:22.000Z",
"asset": {
"name": "staging.acme.co",
"domainId": "ckdom456…",
"domainName": "acme.co"
}
},
{
"id": "ckbucket789…",
"kind": "bucket",
"severity": "critical",
"status": "open",
"evidence": "World-listable bucket acme-prod-backups (us-east-1), discovered via CNAME chain.",
"nextAction": "Restrict the bucket policy/ACL and rotate any leaked credentials.",
"lastSeenAt": "2026-04-11T18:02:09.000Z",
"asset": {
"name": "acme-prod-backups",
"domainId": "ckdom456…",
"domainName": "acme.co"
}
}
],
"nextCursor": "eyJsYXN0U2VlbkF0IjoiMjAyNi0wNC0xMVQxODowMjowOS4wMDBaIiwiaWQiOiJrYnVjazEyMyIsImtpbmQiOiJidWNrZXQifQ"
}- id
- Stable identifier for the finding — useful for cursor pagination and idempotent ticket creation.
- kind
- Either "subdomain" or "bucket" — the asset class the finding belongs to.
- severity
- Always lowercase: "critical" | "high" | "medium" | "low". Buckets are mapped to synthetic severities at the API edge.
- status
- Always lowercase: "open" | "fixed" | "suppressed". Reflects the suppression-aware derivation published with the dashboard.
- evidence
- One-line evidence snippet (e.g. "Certificate Transparency log entry for staging.acme.co — current host status: LIVE.").
- nextAction
- Suggested next-step remediation. Static one-liner you can drop into a Jira ticket body.
- lastSeenAt
- When the finding was last observed, ISO-8601 UTC. Drives keyset ordering.
- asset
- { name, domainId, domainName } — the asset (subdomain host or bucket name) and its enrolled parent domain.
- nextCursor
- Opaque token to pass as ?cursor= on the next page; null when this is the last page.
From your terminal.
# Export the cookie your browser keeps for app.forewatch.example # (the better-auth .session_token cookie). Then page through results. curl -s -G \ --cookie "better-auth.session_token=$FOREWATCH_SESSION" \ "https://app.forewatch.example/api/v1/findings?limit=20&severity=critical,high"
# In CI, export a bearer token issued for your service account. export FOREWATCH_TOKEN="…" curl -s -G \ -H "Authorization: Bearer $FOREWATCH_TOKEN" \ "https://app.forewatch.example/api/v1/findings?limit=20"
# No cookie, no bearer — the route handler rejects you.
$ curl -i "https://app.forewatch.example/api/v1/findings"
HTTP/1.1 401 Unauthorized
content-type: application/json
{"error":"Unauthorized"}Cookies and bearer tokens are interchangeable — the platform's better-auth session middleware accepts either. The 401 response above is what you get when neither is present.
Re-supply nextCursor until it is null.
Responses include a nextCursor opaque token whenever there is at least one more page. Pass it back as ?cursor= on the next request; when the field is null, you have read the last page for this filter.
- 01Request page 1Hit the endpoint with no
?cursor=— get20items. - 02Re-supply the cursorPass the response's
nextCursorback as?cursor=. - 03Stop when nullA
nextCursor: nullmeans you hit the last page.
What you get when it does not work.
- 401No session — missing cookie or bearer token.
- 403Cross-tenant ?domain= — the domain is enrolled to a different user.
- 400Unknown severity / status token, malformed cursor, non-integer limit, or limit > 100.