Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.oathnet.org/llms.txt

Use this file to discover all available pages before exploring further.

Overview

Structured filters are the canonical search grammar behind:
  • GET /service/v2/breach/search
  • GET /service/v2/stealer/search
  • GET /service/v2/victims/search
  • POST /service/v2/ai/filter responses
  • query_config on exports, bulk search, and scanners
Use this guide as the source of truth for filter, filter_id, operator behavior, and filter-related 400 errors.

Start Here

Use the simplest option that fits your case:
  1. If you only need simple exact filters like email, domain, country, or db name, use normal query params.
  2. If you need or, in, not_in, contains, starts_with, ends_with, wildcard, range filters, or exists, use a structured filter.
  3. If you want to type plain English like “US gmail users older than 18”, use the AI filter endpoint and then search with filter_id.
  4. If you are saving a search for scanners, bulk search, or exports, use query_config.
The frontend follows the same idea:
  • keep simple exact rules as normal flat params
  • switch to a structured filter when flat params are no longer enough
  • keep filter_id only when there is no real filter to send yet

1. Simple Search: Use Normal Query Params

This is the easiest option. Use it when every condition is a simple exact match.
curl -G "https://oathnet.org/api/service/v2/breach/search" \
  -H "x-api-key: YOUR_API_KEY" \
  --data-urlencode "q=alice@example.com" \
  --data-urlencode "country[]=US" \
  --data-urlencode "dbname[]=linkedin.com" \
  --data-urlencode "page_size=50"
Typical flat params:
  • breach: email[], email_domain[], country[], city[], dbname[], phone[]
  • stealer: domain[], subdomain[], username[], password[], email[], log_id
  • victims: username[], email[], ip[], hwid[], discord_id[], total_docs_min
If you need any kind of nested logic, partial text match, range, or existence check, move to the next option.

2. Complex Search: Use filter

Use a structured filter when normal query params are not enough. Good examples:
  • country is US OR CA
  • email ends with @gmail.com
  • age is at least 18
  • instagram exists
  • (country is US AND dbname is linkedin.com) OR password contains hunter
You can send filter in two ways.

Option A: GET with filter in the query string

Pass filter as a JSON-encoded string:
curl -G "https://oathnet.org/api/service/v2/breach/search" \
  -H "x-api-key: YOUR_API_KEY" \
  --data-urlencode 'filter={"field":"country","operator":"eq","value":"US"}'

Option B: POST to the same search route with a JSON body

This is the pattern the frontend uses for complex filters. The response shape is the same as GET /search.
const response = await fetch(
  "https://oathnet.org/api/service/v2/breach/search?page_size=50&date_field=indexed_at",
  {
    method: "POST",
    headers: {
      "x-api-key": API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      filter: {
        and: [
          { field: "country", operator: "eq", value: "US" },
          { field: "email", operator: "ends_with", value: "@gmail.com" },
        ],
      },
    }),
  }
);
The body only needs:
  • filter
  • filter_id
Things like page_size, cursor, sort, from, to, and date_field can stay in the query string.

Step 1: Ask the AI filter endpoint to build the filter

curl -X POST "https://oathnet.org/api/service/v2/ai/filter" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "index": "breach",
    "query": "US gmail users older than 18"
  }'
Example response:
{
  "filter_id": "0123456789abcdef01234567",
  "filter": {
    "and": [
      { "field": "country", "operator": "eq", "value": "US" },
      { "field": "email_domain", "operator": "eq", "value": "gmail.com" },
      { "field": "age", "operator": "gte", "value": "18" }
    ]
  }
}

Step 2: Search with the returned filter_id

curl -X POST "https://oathnet.org/api/service/v2/breach/search?page_size=50" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "filter_id": "0123456789abcdef01234567"
  }'

Step 3: Optional: edit the filter and send your own filter

If you want to change the AI result before searching, send a real filter instead of relying only on the stored context.

4. Saved Jobs: Use query_config

query_config is the JSON version of a search. It is used by:
  • scanners
  • bulk search
  • exports

Simple query_config

For JSON query_config objects, prefer canonical keys such as domain, email_domain, and dbname. Bracketed variants such as domain[] are still accepted for parity with query-string filters.
{
  "q": "alice@example.com",
  "wildcard": true,
  "email": "alice@example.com",
  "dbname[]": ["collection_a", "collection_b"],
  "logic": "and",
  "extra_params": {
    "sort": "-indexed_at",
    "fields[]": ["email", "password", "dbname"]
  }
}

Complex query_config

When the logic is too complex for simple flat keys, put a real structured filter inside query_config.filter.
{
  "filter": {
    "or": [
      {
        "and": [
          { "field": "email", "operator": "eq", "value": "alice@example.com" },
          { "field": "country", "operator": "eq", "value": "US" }
        ]
      },
      { "field": "password", "operator": "contains", "value": "hunter2" }
    ]
  }
}

Filter Grammar

Every filter is built from just three shapes:
  • a leaf rule with field
  • an and group
  • an or group

Leaf node

{
  "field": "country",
  "operator": "eq",
  "value": "US"
}

AND group

{
  "and": [
    { "field": "country", "operator": "eq", "value": "US" },
    { "field": "dbname", "operator": "eq", "value": "linkedin.com" }
  ]
}

OR group

{
  "or": [
    { "field": "country", "operator": "eq", "value": "US" },
    { "field": "country", "operator": "eq", "value": "CA" }
  ]
}

Nested example

{
  "and": [
    { "field": "country", "operator": "eq", "value": "US" },
    {
      "or": [
        { "field": "email", "operator": "ends_with", "value": "@gmail.com" },
        { "field": "email", "operator": "ends_with", "value": "@outlook.com" }
      ]
    }
  ]
}

Merge Order

Requests support this merge order:
  1. filter_id loads a stored transient filter context when it still exists.
  2. An explicit filter replaces that stored filter.
  3. Flat filters such as email[], domain[], dbname[], or log_id are merged on top.
  4. Pagination and sort settings such as cursor, page_size, fields[], from, to, and sort still apply normally.
In simple terms:
  • start from a saved or AI-generated filter_id
  • send your own filter if you want to take control
  • still add normal query params if needed

Operator Guide In Plain Language

OperatorValue typeMeaningNotes
eqstringExact or field-aware equalityIf the value contains * or ?, it behaves like a wildcard pattern instead of a literal exact match
neqstringNot equalInverts the normal equality translation
instring arrayMatch any listed valueArray required
not_instring arrayExclude any listed valueArray required
containsstringSubstring matchRejected on date and IP fields
starts_withstringPrefix matchRejected on date and IP fields
ends_withstringSuffix matchRejected on date and IP fields
wildcardstringWildcard pattern using * and ?Rejected on date and IP fields
gtstringGreater thanUse stringified numeric/date values
gtestringGreater than or equalUse stringified numeric/date values
ltstringLess thanUse stringified numeric/date values
ltestringLess than or equalUse stringified numeric/date values
existsbooleanField exists or is missingtrue means present, false means missing, and omitted value defaults to true

Quick examples

{ "field": "country", "operator": "eq", "value": "US" }
{ "field": "country", "operator": "in", "value": ["US", "CA", "GB"] }
{ "field": "email", "operator": "ends_with", "value": "@gmail.com" }
{ "field": "instagram", "operator": "exists", "value": true }
{ "field": "age", "operator": "gte", "value": "18" }

Common Cases

Pattern searches

  • contains, ends_with, and wildcard require at least 2 non-wildcard characters
  • patterns like "*", "*a*", or "?" are rejected with 400
  • starts_with uses prefix semantics
{ "field": "username", "operator": "starts_with", "value": "admin" }
{ "field": "password", "operator": "contains", "value": "hunter" }
{ "field": "email", "operator": "wildcard", "value": "*@gmail.com" }

Exists / missing checks

{ "field": "instagram", "operator": "exists", "value": true }
{ "field": "instagram", "operator": "exists", "value": false }

Numeric and date ranges

Use string values that match the public field format:
  • numeric-style fields such as age should use numeric strings like "18"
  • date-style fields such as date_birth should use ISO-style dates like "1990-01-01"
  • datetime fields such as indexed_at should use RFC3339 timestamps like "2026-04-18T00:00:00Z"
{ "field": "age", "operator": "gte", "value": "18" }
{ "field": "date_birth", "operator": "lt", "value": "1990-01-01" }

in and not_in

Use arrays when you already know multiple values:
{ "field": "dbname", "operator": "in", "value": ["linkedin.com", "twitter.com"] }
{ "field": "country", "operator": "not_in", "value": ["US", "CA"] }

Domain-only email filters

When you already know the mail domain, prefer the dedicated public field:
{ "field": "email_domain", "operator": "eq", "value": "gmail.com" }

Step-By-Step Examples

Example: exact filters only

curl -G "https://oathnet.org/api/service/v2/stealer/search" \
  -H "x-api-key: YOUR_API_KEY" \
  --data-urlencode "domain[]=google.com" \
  --data-urlencode "email[]=admin@google.com" \
  --data-urlencode "has_log_id=true"

Example: one structured rule

{ "field": "email", "operator": "ends_with", "value": "@gmail.com" }

Example: AND two rules together

{
  "and": [
    { "field": "country", "operator": "eq", "value": "US" },
    { "field": "age", "operator": "gte", "value": "18" }
  ]
}

Example: OR two rules together

{
  "or": [
    { "field": "email_domain", "operator": "eq", "value": "gmail.com" },
    { "field": "email_domain", "operator": "eq", "value": "outlook.com" }
  ]
}

Example: nested logic

{
  "or": [
    {
      "and": [
        { "field": "country", "operator": "eq", "value": "US" },
        { "field": "dbname", "operator": "eq", "value": "linkedin.com" }
      ]
    },
    { "field": "password", "operator": "contains", "value": "hunter2" }
  ]
}

Example: send the filter with POST

curl -X POST "https://oathnet.org/api/service/v2/breach/search?page_size=50" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "filter": {
      "or": [
        {
          "and": [
            { "field": "country", "operator": "eq", "value": "US" },
            { "field": "dbname", "operator": "eq", "value": "linkedin.com" }
          ]
        },
        { "field": "password", "operator": "contains", "value": "hunter2" }
      ]
    }
  }'

Example: filter_id plus your own exact params

This is valid:
curl -G "https://oathnet.org/api/service/v2/stealer/search" \
  -H "x-api-key: YOUR_API_KEY" \
  --data-urlencode 'filter_id=0123456789abcdef01234567' \
  --data-urlencode 'email[]=admin@google.com' \
  --data-urlencode 'has_log_id=true'

Example: filter_id plus a replacement filter

This is also valid:
await fetch("https://oathnet.org/api/service/v2/breach/search?page_size=50", {
  method: "POST",
  headers: {
    "x-api-key": API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    filter_id: "0123456789abcdef01234567",
    filter: {
      field: "country",
      operator: "eq",
      value: "FR",
    },
  }),
});

Choosing Fields

Field availability is endpoint-specific. A field accepted by breach search is not automatically valid for stealer or victims.
  • breach search commonly uses fields such as email, email_domain, username, country, city, dbname, date_birth, and age
  • stealer search commonly uses fields such as domain, subdomain, path, username, password, email, ip, hwid, discord_id, and log_id
  • victims search commonly uses fields such as username, email, ip, hwid, discord_id, log_id, and total_docs
Use the endpoint pages for each search surface when choosing fields, and use the breach autocomplete endpoints when you need help discovering db names or field/value coverage. query_config reuses the same filter system, with a few extra rules:
  • filter can be a JSON object or a JSON string
  • flat filters can be sent with canonical JSON keys such as domain, email_domain, or dbname
  • bracketed keys such as domain[] are also accepted
  • scanners reject runtime-only keys such as from, to, cursor, page_size, format, debug, and search_id
  • scanners always monitor on indexed_at
  • exports and bulk search allow from, to, date_field, and sort because those jobs serialize full searches
In practice:
  • start flat when the saved search is simple
  • switch to query_config.filter when the saved search becomes complex

Limits

  • filter_id must be a 24-character hex string
  • maximum nesting depth is 2
  • maximum leaf conditions is 50
  • value is required for every operator except exists
  • in and not_in require arrays

Common 400 Errors

Typical structured-filter validation failures include:
  • malformed JSON in filter
  • a node that mixes field with and or or
  • invalid operator names
  • disallowed fields for the selected endpoint
  • contains, ends_with, or wildcard patterns that are too short
  • using pattern operators on date or IP fields
  • nesting deeper than two levels
  • more than fifty leaf rules

What The Frontend Does

The frontend uses three practical rules that are helpful for API clients too:
  1. If a filter can be represented as normal exact-match params, keep it simple and send flat params.
  2. If the filter needs nested logic or advanced operators, send a real structured filter.
  3. If both filter_id and a real filter are available for a saved config, prefer the real filter as the canonical version.

V2 Breach Search

Search breach data with structured filters

V2 Stealer Search

Search stealer records with flat or structured filters

Search Victims

Search victim summaries using the same filter grammar

Create AI Filter

Generate a reusable filter from natural language