Retrieve keyword-based leak statistics

GET
/leaks/keyword/stats

Retrieve all leak statistics for the current organization's active keywords.

Generic accounts such as admin, root, test are counted in these figures, for an API key as in the application.

Returns the same format as /leaks/stats (StatsResponse), computed from keyword search results on hash_registry (full ingestion history, folded per hash). The rows matching active keywords (excluding the org's own monitored root domains) are fed through the same calculate_all_statistics() pipeline.

If the organization has no active keywords, returns an empty stats response.

Parameters:

ParameterDefaultDescription
granularitydayTime granularity (unused in keyword stats, kept for API consistency)
start_date / end_dateall-timeDate range filter; an explicit value narrows the window

Plus all standard DynamicFilters.

Accepts ?tenant_id_override=<child_tenant_id> for an MSSP workspace: the active keywords, excluded roots and paid-plan check then resolve from the CHILD tenant instead of the caller's own org (tenant_id_override=all is rejected with 400, a keyword search has no defined meaning across a whole portfolio).

Example:

GET /leaks/keyword/stats?start_date=2026-03-01&end_date=2026-03-28

Authorization

ApiKeyAuth
X-API-Key<token>

API key for authentication

In: header

Query Parameters

granularity?Granularity

Time granularity (kept for API consistency)

Default"day"
Value in"hour" | "day"
include_all_time?Include All Time

When true, include all-time aggregates (all_time_total_leaks, all_time_unique_users, all_time_unique_hosts, all_time_unique_passwords) computed without date filtering. Used by the cockpit landing page.

Defaultfalse
username?|array<string>|

Username of the leaked user (can be phone, email, ID, ...)

type?|array<string>|

Type of leak (combo, stealer)

hash?|array<string>|

Uniq identifier for the concatenation of : username, password and domain

upload_stealed?||array<|string>|

Upload date on stealed, ISO 8601, pattern YYYY-MM-DD

upload_date?||array<|string>|

Upload date on the plateform the credential was found, ISO 8601, pattern YYYY-mm-dd

log_date?||array<|string>|

Log date of the device at compromized moment (if applicable, stealer only)

start_date?||

Start date to search from leaks, format: YYYY-mm-dd (default: today - 14days at 0:00am)

end_date?||

End date to search leaks from, format YYYY-mm-dd (default: today)

host?|array<string>|

URL of the leaked data

domain?|array<string>|

FQDN of the leaked data

local_part?|array<string>|

Local part of the username section (if applicable, email only)

protocol?|array<string>|

Protocol identified (if applicable)

email_domain?|array<string>|

Email domain to filter on (if multiple email domains declared)

root_domain?|array<string>|

Root domain to filter on (if multiple root domains declared)

machine_id?|array<string>|

Machine ID (if applicable, stealer only)

computer_name?|array<string>|

Computer name (if applicable, stealer only)

hardware_id?|array<string>|

Hardware ID (if applicable, stealer only)

machine_user?|array<string>|

Machine user (if applicable, stealer only)

ip_address?|array<string>|

IP address (if applicable, stealer only)

country?|array<string>|

Country (if applicable, stealer only)

software?|array<string>|

Software (if applicable, stealer only)

stealer_name?|array<string>|

Stealer name (if applicable)

source?|array<string>|

Opaque source label(s) to filter on, of the form Source-<6 hex> (as returned by the sources breakdown). The raw telegram_channel is never exposed: the label is matched back against the same MD5-prefix expression server-side.

keyword?|array<string>|

Keyword to filter on (only active keywords for tenant)

match_type?|array<string>|

Match type for leaks_matched table (root_domain or email_domain)

not_root_domain?|array<string>|

Root domains to exclude (NOT IN filter)

not_email_domain?|array<string>|

Email domains to exclude (NOT IN filter)

not_domain?|array<string>|

Domains to exclude (NOT IN filter)

not_type?|array<string>|

Types to exclude

not_software?|array<string>|

Software to exclude

not_stealer_name?|array<string>|

Stealer names to exclude

not_protocol?|array<string>|

Protocols to exclude

not_country?|array<string>|

Countries to exclude

not_machine_id?|array<string>|

Excludes the credentials whose machine ID is one of these values (NOT IN filter, stealer only)

not_computer_name?|array<string>|

Excludes the credentials whose computer name is one of these values (NOT IN filter, stealer only)

not_hardware_id?|array<string>|

Excludes the credentials whose hardware ID is one of these values (NOT IN filter, stealer only)

not_machine_user?|array<string>|

Excludes the credentials whose machine user is one of these values (NOT IN filter, stealer only)

not_ip_address?|array<string>|

Excludes the credentials whose IP address is one of these values (NOT IN filter, stealer only)

first_seen_date?|

Show only hashes whose first appearance (min upload_stealed) falls on this exact date. Format: YYYY-MM-DD.

first_seen_since?|

Show only hashes whose first appearance (min upload_stealed) is on or after this date. Format: YYYY-MM-DD.

limit?|

Limit result length

min_occurrences?|

Minimum number of sources (source_count >= N)

Response Body

application/json

application/json

curl -X GET "https://api.stealed.io/leaks/keyword/stats"
null
{
  "detail": [
    {
      "loc": [
        "string"
      ],
      "msg": "string",
      "type": "string"
    }
  ]
}

Retrieve every occurrence (source + file) for a specific leak hash GET

Retrieve every distinct detection of a deduplicated credential — one row per `(telegram_channel, file_name)` pair — read directly from the tenant-routed `leaks_matched_*` table (no separate occurrence store, no JOIN). The row count EQUALS the `occurrences` figure shown in the `/leaks/details` list (same grain), across ALL types (Combo and Stealer). Use this after `/leaks/details` to drill into a credential and see each detection. The proprietary `telegram_channel` and `file_name` are returned (`channel`, `file_name`) only to a USER SESSION of an organisation holding the `source_reveal` feature (on by default for a workspace, off for a client), never to an API key, with the name to show for it (`source_name`, null when none was kept). Every user session, never an API key, also reads the origin of each detection (`source_type`: `telegram`, `forum`, or `other` for a source that is neither). Otherwise the source is replaced by a stable opaque id (`opaque_source_id`, `"Source #" + md5(channel)[:6]`) and, when the detection carried a published file, an opaque lot id (`opaque_lot_id`, `md5(file_name)[:6]`). Backfilled / historical detections have no file, hence no lot. `computer_name` is never selected nor returned. `date` is `max(upload_stealed)` for that detection, i.e. **when Stealed ingested it** — the same clock as the list's `first_seen` / `last_seen`, so the two views can never contradict each other. It is deliberately NOT the source's own publication timestamp (`upload_date`, the Telegram message date), which runs hours to days ahead of ingestion and is unset on some rows. **Parameters:** - `hash` (required): The leak hash from the `/leaks/details` response - `tenant_id_override` (optional): workspace (MSSP) only — inspect a child tenant's detail; validated against the caller's portfolio (403/404 otherwise) Tenant isolation: the query is scoped `WHERE tenant_id = <server-derived> AND hash = <param>`, so a hash belonging to another tenant returns nothing. **Response:** ```json { "total_occurrences": 2, "occurrences": [ {"date": "2026-06-01T10:30:00", "source": "Source #a1b2c3", "lot": "d4e5f6", "type": "Stealer", "country": "FR", "stealer_name": "RedLine", "software": "Chrome", "ip_address": "1.2.3.4", "machine_user": "admin", "machine_id": "…", "protocol": "https"}, {"date": "2026-05-20T08:00:00", "source": "Source #99aa11", "lot": null, "type": "Combo", "country": null, "stealer_name": null, "software": null, "ip_address": null, "machine_user": null, "machine_id": null, "protocol": null} ] } ``` A user session also reads, on each row, `"source_type": "forum"` (or `"telegram"`, `"other"`); a session allowed to see the sources reads the names next to it, for example `"source_name": "example-forum", "channel": "example-forum", "file_name": null`. **Example:** ```bash GET /leaks/details/occurrences?hash=5d41402abc4b2a76b9719d911017c592 ```

Paginated keyword search results GET

Retrieve paginated leak details matching the organization's active keywords. Generic accounts such as `admin`, `root`, `test` are counted in these figures, for an API key as in the application. Returns the same paginated envelope as `/leaks/details`. The row shape differs slightly: keyword rows are folded per hash from `hash_registry`, so they carry `local_part` (derived) and no per-credential value sets (`softwares`, `ip_addresses`, ...). This endpoint has an additional `matched_keyword` field indicating which keyword matched each row. Searches `hash_registry` (full ingestion history) for rows where the keyword's tokens appear in `domain`/`host` (same predicate as the `idx_kw_text` index), for any active keyword, excluding the org's own monitored root domains. If the organization has no active keywords, returns an empty paginated response. **Parameters:** | Parameter | Default | Description | |-----------|---------|-------------| | `page` | 1 | Page number | | `page_size` | 50 | Items per page (max 200) | | `search` | - | Full-text search across username, host, domain, root_domain | | `sort_by` | `last_seen` | Sort order: `last_seen` (most recent first) | | `start_date` / `end_date` | all-time | Date range filter; an explicit value narrows the window | Plus all standard DynamicFilters. Accepts `?tenant_id_override=<child_tenant_id>` for an MSSP workspace: the active keywords, excluded roots and paid-plan check then resolve from the CHILD tenant instead of the caller's own org (`tenant_id_override=all` is rejected with 400). A Référencé child's rows render with `username` masked (`x***@domain`, same redaction as `/leaks/details`); an Activé child's `username` renders in clear, `password` stays masked either way. **Response Format:** ```json { "data": [ { "username": "[email protected]", "password": "a91f3c8b2d47", "password_masked": true, "type": "Stealer", "first_seen": "2026-01-20", "last_seen": "2026-03-15", "host": "acme-corp-recrute.talent-soft.com", "domain": "acme-corp-recrute.talent-soft.com", "local_part": "user", "protocol": "https", "email_domain": "example.com", "root_domain": "talent-soft.com", "country": "FR", "software": "chrome, profile: 0", "stealer_name": "RedLine", "hash": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4", "machine_id": "DESKTOP-ABC1234", "computer_name": "LAPTOP-XYZ", "hardware_id": "hwid-1234-5678", "machine_user": "john.doe", "ip_address": "192.168.1.10", "source_count": 3, "matched_keyword": "acme-corp" } ], "total": 12345, "page": 1, "page_size": 50, "total_pages": 247 } ``` **Examples:** ```bash # Basic paginated request GET /leaks/keyword/search?page=1&page_size=50 # With search filter GET /leaks/keyword/search?search=admin&page=1&page_size=50 # With date range GET /leaks/keyword/search?start_date=2026-03-01&end_date=2026-03-28 ```