← LeakWatch

Documentation

Query the secrets LeakWatch detects in public repositories, scan your own content, and fail a build before a key reaches a public repo — from your scripts, your pipeline, or an agent. 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/scan/batchSolo / TeamScan a whole set of files in one call. Nothing is stored.
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.

Projects & site scans

A project groups the repositories you own with the domains you deploy, so one report can answer the question neither half answers alone: is a leaked key still live? A key in an old commit may have been revoked; the same key served in your production bundle has not. Attaching a repository checks ownership exactly as a deep scan does, and a domain must be proven — declaring it is not proving it — before any scan touches it.

EndpointPlanDescription
GET/projectsAnyYour projects, each with its domains and repositories.
POST/projectsAnyCreate a project — the group that ties repositories to the sites you deploy.
GET/projects/{id}AnyOne project and its assets.
DELETE/projects/{id}AnyDelete a project and its assets. Findings already detected are kept.
POST/projects/{id}/domainsAnyDeclare a domain and get the DNS / .well-known proof to publish. Born unverified.
GET/projects/{id}/domains/{domain_id}AnyA domain and its proof instructions.
POST/projects/{id}/domains/{domain_id}/verifyAnyCheck the proof over the network and mark the domain verified.
DELETE/projects/{id}/domains/{domain_id}AnyRemove a domain from the project.
POST/projects/{id}/reposAnyAttach one of your repositories. Ownership is checked, never assumed.
DELETE/projects/{id}/repos/{repo_id}AnyDetach a repository from the project.
GET/projects/{id}/reportAnyUnified report across the project's assets, with correlations first — a key committed in a repository and served live on your site.
GET/site-scans/quotaAnyRemaining site-scan quota and when the next one frees up.
GET/site-scansAnyYour recent site scans.
POST/site-scansAny (quota)Scan a deployed site whose domain you have verified. Free plans get one per window.
GET/site-scans/{id}AnyProgress of a site scan — poll while queued or running.
GET/site-scans/{id}/reportAnySecrets served by the site, in full, plus configuration issues (exposed .env, readable .git, missing headers).

Prove a domain before scanning it

Adding a domain returns the record to publish — a DNS TXT or a /.well-known file. Publish it, then call verify. The proof matters: a site scan probes paths like /.env and /.git/HEAD and returns what it finds in full. On someone else's domain that would be an attack, not an audit, so an unverified domain is refused with 403.

# 1. Declare the domain (returns the proof to publish)
curl -X POST https://leakwatch.net/api/v1/projects/<project_id>/domains \
  -H "Authorization: Bearer lw_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"domain": "example.com"}'

# 2. Once the record is live, verify it
curl -X POST \
  https://leakwatch.net/api/v1/projects/<project_id>/domains/<domain_id>/verify \
  -H "Authorization: Bearer lw_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"method": "dns_txt"}'

# 3. Scan the deployed site (returns immediately, status "queued")
curl -X POST https://leakwatch.net/api/v1/site-scans \
  -H "Authorization: Bearer lw_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"domain": "example.com"}'

# 4. Poll the scan, then read the unified project report
curl https://leakwatch.net/api/v1/projects/<project_id>/report \
  -H "Authorization: Bearer lw_live_your_key_here"

The project report puts correlations first: each is one secret found on both sides of your deployment — committed and served — so it was neither rotated nor removed. Those are the findings that need action today, ahead of everything ranked by severity below them. As with scan reports, values are returned in full, since these are your own verified assets. Domain ownership is re-checked when the report is read, not only when the scan is started.

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
    }
  ]
}

Your content is never stored. It is scanned in memory and discarded when the response is sent, and it is never forwarded to a third party — detection is local pattern matching, not a model call. What we do keep is the result: how many secrets, of which types and severities, on how many bytes. That is what feeds the CI/CD tab of your dashboard and GET /scan/history, and it is all there is — no diff, no secret value, no line number. Bodies are capped at 1 MiB on Free and 5 MiB on paid plans; 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.

Quota. Free keys get 50 content scans per day (UTC), and the response carries quota_remaining so a job can warn before it hits the wall. Once it does, the answer is 402 — not 429, which stays reserved for going too fast. Paid plans have no daily quota and may send bodies up to 5 MiB.

A CI gate, without installing anything

No action to add, no binary to vendor, no runner image to rebuild: the job needs curl and jq, which every CI image already ships. Put your key in a secret named LEAKWATCH_API_KEY and fail the build on a non-zero count.

#!/bin/sh
# Fails the build if the diff about to be merged contains a secret.
set -eu

DIFF=$(git diff --unified=0 origin/main...HEAD)

COUNT=$(printf '%s' "$DIFF"   | jq -Rs '{content: ., filename: "diff"}'   | curl -sS -X POST https://leakwatch.net/api/v1/scan/content       -H "Authorization: Bearer $LEAKWATCH_API_KEY"       -H "Content-Type: application/json"       --data-binary @-   | jq '.count')

if [ "$COUNT" -ne 0 ]; then
  echo "LeakWatch: $COUNT secret(s) in this diff — refusing to merge."
  exit 1
fi

Dropped into GitHub Actions, that is a single step — and the same script runs unchanged in GitLab CI, CircleCI, or a local pre-push hook:

- name: Secret scan
  env:
    LEAKWATCH_API_KEY: ${{ secrets.LEAKWATCH_API_KEY }}
  run: sh ci/leakwatch.sh

Scan the diff rather than the whole tree: it is what changed that can leak, the payload stays small, and a key committed years ago will not fail every build from now on. For the whole tree — a first audit, or a nightly job — use the batch endpoint below.

Full recipes — GitHub Actions, GitLab CI, CircleCI, a pre-push hook, and what to do when the API answers something other than 200 — are in the CI/CD tab.

Many files in one call

POST /scan/batch takes a list of files and returns one result per file, in the order you sent them. It exists so that scanning a repository does not mean splitting the payload yourself — which is where "no install" quietly stops being true. Requires a paid plan; up to 200 files, 5 MiB total.

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

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

Over either cap the whole call is refused rather than truncated: a partial scan reported as a pass is worse than no scan at all.

Past runs

GET /scan/history returns your past runs, most recent first — counts only, kept for a bounded window that the response states in retention_days. The same list is the CI/CD tab of your dashboard.

curl https://leakwatch.net/api/v1/scan/history \
  -H "Authorization: Bearer lw_live_your_key_here"

{
  "retention_days": 90,
  "runs": [
    {
      "id": "0d9f…",
      "created_at": "2026-08-25T09:12:44Z",
      "endpoint": "content",
      "filename": "diff",
      "file_count": 1,
      "bytes_scanned": 8213,
      "secret_count": 1,
      "by_severity": { "critical": 1 },
      "by_type": { "AWS Access Token": 1 }
    }
  ]
}

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 — a CI runner keeps its own budget.
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.
402Valid key, but the plan does not cover it — a paid-only endpoint, or a free daily quota that ran out. Distinct from 429 on purpose: waiting will not help.
413Body over your plan's ceiling (1 MiB free, 5 MiB paid), or too many files in a batch. Nothing is scanned and no quota is consumed.
422Invalid parameters (unknown source, malformed account).
429Rate limit exceeded.

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