← LeakWatch

API documentation

Query the secrets LeakWatch detects in public repositories, from your own scripts, CI, or agents. REST over HTTPS, JSON responses, an OpenAPI schema for machine consumption.

Interactive reference (Swagger) → · OpenAPI schema

Quickstart

The read endpoints work without any credentials. Try one right now:

curl https://leakwatch.net/api/v1/leaks/stats

Every path below is relative to the base URL https://leakwatch.net/api/v1.

Authentication

Account features — and the higher rate limits — need an API key. Create one in your dashboard, then send it as a bearer token:

curl https://leakwatch.net/api/v1/leaks/search?account=octocat&source=github \
  -H "Authorization: Bearer lw_live_your_key_here"

The key is shown once, when you create it: we store only a hash, so a lost key cannot be recovered — revoke it and create another. Treat it like a password; anyone holding it acts as your account. If you ever leak one, revoking it from the dashboard takes effect immediately.

Public endpoints

These read from the public detection database and work with or without a key. Everything here is masked and detached from its repository.

EndpointAuthDescription
GET/leaks/recentNoneThe 50 most recent detections. Secrets masked, repository never revealed.
GET/leaks/searchOptionalAggregated counts for an account: total, distinct repositories, severity breakdown. Takes account and source.
GET/leaks/statsNoneGlobal statistics: all-time total, last 24 hours, severity breakdown.
GET/detectorsNoneCatalogue of every secret type the engine recognises, with its severity.
GET/healthNoneLiveness probe.
GET/versionNoneAPI version.

Example — recent detections

curl https://leakwatch.net/api/v1/leaks/recent

{
  "leaks": [
    {
      "type": "AWS Access Token",
      "forge": "github",
      "masked_secret": "AKIA...",
      "discovered_at": "2026-08-12T09:14:22Z"
    }
  ]
}

Example — leaks for an account

curl "https://leakwatch.net/api/v1/leaks/search?account=octocat&source=github"

{
  "account": "octocat",
  "source": "github",
  "total": 3,
  "distinct_repos": 2,
  "by_severity": { "critical": 2, "high": 0, "medium": 1, "low": 0 }
}

Your account

These endpoints act on your own data and always require a key. What you get back follows your plan — GET /me tells you which one applies and how much quota is left, so integrations can check rather than guess.

EndpointPlanDescription
GET/meAnyYour account: plan, linked identities, rate limit and remaining deep-scan quota.
GET/me/leaksAnyEvery secret found across your verified identities. Full values on paid plans only.
GET/scansAnyYour recent deep scans.
POST/scansAny (quota)Scan the full git history of one of your repositories. Free plans get one per window.
GET/scans/{id}AnyProgress of a scan — poll while queued or running.
GET/scans/{id}/reportAnyEverything currently known about the scanned repository, values in full.
GET/monitorsAnyRepositories you have under continuous monitoring.
POST/monitorsSolo / TeamPut one of your public repositories under continuous monitoring.
DELETE/monitors/{id}Solo / TeamStop watching a repository. Findings already detected are kept.
GET/monitors/{id}/findingsSolo / TeamSecrets found in one of your monitored repositories, values in full.
POST/scan/contentAnyScan text you submit for secrets. Nothing is stored — ideal for a CI gate.
POST/me/leaks/{id}/validateSolo / TeamRe-test one of your own leaked keys against its provider.

Example — scan your own repository

# Start the scan (returns immediately, status "queued")
curl -X POST https://leakwatch.net/api/v1/scans \
  -H "Authorization: Bearer lw_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"repo_full_name": "octocat/hello-world"}'

# Poll until status is "success", then read the report
curl https://leakwatch.net/api/v1/scans/<id>/report \
  -H "Authorization: Bearer lw_live_your_key_here"

Full values and your plan

In GET /me/leaks, the secret, commit_url and still_active fields are populated on paid plans and null on the free plan — the fields are always present, so your parsing does not change. Scan reports and monitored-repository findings always return values in full: those are repositories you own, and a masked value would not tell you which key to revoke.

Scan your own content

Send text — a diff, a file, an environment block — and get back the secrets found in it. The same engine and the same /detectors catalogue that power our public scanning. Useful as a pre-push hook or a CI gate that fails a build before a key ever reaches a public repository.

curl -X POST https://leakwatch.net/api/v1/scan/content \
  -H "Authorization: Bearer lw_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"content": "AWS_SECRET=AKIAIOSFODNN7EXAMPLE\n", "filename": ".env"}'

{
  "filename": ".env",
  "count": 1,
  "bytes_scanned": 41,
  "secrets": [
    {
      "type": "AWS Access Token",
      "severity": "critical",
      "value": "AKIAIOSFODNN7EXAMPLE",
      "line": 1
    }
  ]
}

Nothing you send is stored. The content is scanned in memory and discarded when the response is sent: no database row is written, and it is never forwarded to a third party — detection is local pattern matching, not a model call. Bodies are capped at 1 MiB; split larger inputs and scan them in parts.

Values are returned in full, since they came from the content you submitted. A hit is a pattern match, not proof the credential is live — this endpoint never tests it against its provider.

Checking whether your own key is still live

That is a separate endpoint, and it works from a leak id rather than a key value: POST /me/leaks/{id}/validate re-tests a credential that was found in one of your repositories, then updates still_active in GET /me/leaks. There is deliberately no endpoint that validates an arbitrary key you paste in: that would be a testing service for stolen credentials, which is not something we are willing to run.

What the API never returns

These are deliberate limits, not gaps. LeakWatch publishes proof that a secret leaked — never a working copy of it:

  • Secret values are masked. Only the type prefix is shown (AKIA…, ghp_…). Full values are reserved for the verified owner of the account they belong to, so they can revoke them.
  • The repository is never named in the public feed — no repo name, no commit URL. Without it, the feed is not a map to live secrets.
  • Liveness is not published. Whether a key still works is the one detail with operational value to an attacker.
  • Search is per account, aggregated. Counts, not a list you can walk. Codeberg lookups are restricted to the account owner.

Rate limits

Limits follow your plan and are counted per key, so two integrations behind one IP each get their own budget. Over the limit, the API answers 429 — back off and retry.

PlanLimit
No keyper IPEnough to try things out and for occasional use.
Free60 req/minCounted per key, not per IP.
Solo300 req/min
Team1,000 req/min

Errors

Errors use standard HTTP status codes and carry a detail field explaining what went wrong.

CodeMeaning
401Missing, malformed, or revoked API key.
403Valid key, but not allowed — e.g. a Codeberg lookup for an account you have not verified.
422Invalid parameters (unknown source, malformed account).
429Rate limit exceeded.

What's next

Webhooks — a URL called whenever a new secret is found in one of your repositories, instead of polling /me/leaks. Want that, or something else? Tell me.

The Swagger reference is generated from the running code — when in doubt, it is the source of truth.