Retrieve keyword-based leak statistics

GET
/leaks/keyword/stats

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

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`, `source_type`) 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. 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} ] } ``` **Example:** ```bash GET /leaks/details/occurrences?hash=5d41402abc4b2a76b9719d911017c592 ```

Paginated keyword search results GET

Retrieve paginated leak details matching the organization's active keywords. 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": "user@example.com", "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 ```