> ## 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.

# Scanners (Automated Monitoring)

> Monitor newly indexed stealer or breach data with email, Discord, or webhook delivery

# Scanners

Scanners let you monitor for newly indexed `stealer` or `breach` results and receive notifications through email, Discord webhooks, or custom HTTP webhooks.

## What Scanners Actually Monitor

<Info>
  Scanners are incremental monitors. They only look for data indexed after the current scanner baseline.
</Info>

* scanners run on an hourly schedule
* the window is based on `indexed_at`, not `pwned_at`
* historical data is not replayed when you create a scanner
* material query changes reset the baseline and begin monitoring from the update time

Use the regular search endpoints when you need to investigate historical data.

## Scanner Types

| Type      | Backing data family | Typical use                                                                |
| --------- | ------------------- | -------------------------------------------------------------------------- |
| `stealer` | Credential records  | Monitor credentials, domains, usernames, passwords, subdomains, or log IDs |
| `breach`  | Breach records      | Monitor db names, names, phone numbers, email domains, and similar fields  |

## Query Config Rules

`query_config` is close to the live v2 search filters, but it is not a raw request replay.

* allowed flat filters depend on `scanner_type`
* `filter` and `filter_id` are supported
* canonical JSON keys such as `domain`, `email_domain`, `dbname`, and `username` are preferred
* bracketed variants such as `domain[]` are also accepted
* runtime-only parameters are rejected because scanners manage them automatically:
  * `from`
  * `to`
  * `cursor`
  * `page_size`
  * `format`
  * `debug`
  * `search_id`
* scanners always search with `date_field=indexed_at`
* scanner configs must contain a durable search anchor, such as a concrete domain, email domain, dbname, username, password hash, log ID, or structured filter leaf
* wildcard-only, `filter_id`-only, or `extra_params`-only configs are rejected because they are not stable enough for hourly monitoring

Simple rule:

* if the scanner can be described with exact-match fields, keep `query_config` flat
* if the scanner needs `or`, `contains`, `exists`, ranges, or nested groups, use `query_config.filter`

See [Structured Filters](/guides/structured-filters) for the shared `filter` grammar, step-by-step examples, and `filter_id` behavior.

## Notifications

| Notification type | Notes                                                         |
| ----------------- | ------------------------------------------------------------- |
| `email`           | Sends to your OathNet account email                           |
| `discord`         | Requires a Discord webhook URL                                |
| `webhook`         | Requires a custom HTTP or HTTPS endpoint reachable by OathNet |

### Webhook Security Modes

* `signed_json`
  Default mode for new custom webhook scanners. OathNet signs the raw request body with the shared secret.
* `signed_encrypted`
  Signed and encrypted delivery for custom webhook scanners. OathNet signs the encrypted body and includes key metadata in headers.
* `api_key`
  Available only on existing scanners. New scanners should use signed delivery.

Webhook URLs are validated aggressively:

* only `http` and `https` are allowed
* IP addresses are rejected
* localhost and private-network hostnames are rejected
* Discord scanners must use a Discord webhook URL

## Creating a Scanner

Create scanners with a normalized `query_config` and delivery target. The
create request should describe what to monitor and where to send notifications;
OathNet owns scheduling, baselines, and run windows.

Required fields are `name`, `scanner_type`, `query_config`, and `notification_type`. `webhook_url` is also required for `webhook` and `discord` notifications.

```json theme={null}
{
  "name": "Example breach monitor",
  "scanner_type": "breach",
  "query_config": {
    "filter": {
      "and": [
        { "field": "email_domain", "operator": "eq", "value": "example.com" },
        { "field": "dbname", "operator": "in", "value": ["twitter.com", "linkedin.com"] }
      ]
    }
  },
  "notification_type": "webhook",
  "webhook_url": "https://alerts.example.com/oathnet",
  "webhook_security_mode": "signed_json",
  "notify_on_zero_results": false
}
```

Before saving a webhook or Discord scanner, validate delivery so bad URLs or
rejected signatures do not create a monitor that immediately fails.

## Delivery Payload

Webhook and Discord notifications include scanner and run metadata plus a sample of matching results. The exact `results.sample` shape depends on `scanner_type`.

```json theme={null}
{
  "event": "scanner.results_found",
  "scanner": {
    "uid": "550e8400-e29b-41d4-a716-446655440000",
    "name": "Example breach monitor",
    "type": "breach",
    "query_config": {
      "email_domain[]": ["example.com"]
    }
  },
  "run": {
    "uid": "660e8400-e29b-41d4-a716-446655440001",
    "search_from": "2026-04-18T00:00:00Z",
    "search_to": "2026-04-18T01:00:00Z"
  },
  "results": {
    "total_count": 5,
    "sample_count": 1,
    "sample": [
      {
        "email": "user@example.com",
        "dbname": "twitter.com",
        "indexed_at": "2026-04-18T00:45:00Z"
      }
    ]
  },
  "search_url": "https://oathnet.org/search?...",
  "timestamp": "2026-04-18T01:00:00Z"
}
```

Use the webhook-security read operation to inspect the configured verification
method and delivery headers for a saved scanner.

Common webhook headers include a delivery ID, timestamp, signature, security mode, key ID, and encryption metadata when `signed_encrypted` is enabled. Verify signatures against the exact raw request body before parsing JSON.

## Verifying Webhook Signatures

For `signed_json` and `signed_encrypted`, never rebuild the JSON before
verification. Read the exact raw HTTP body bytes, then verify the
`X-OathNet-Signature` header with HMAC-SHA256.

Signature inputs:

| Field         | Source                                                   |
| ------------- | -------------------------------------------------------- |
| `timestamp`   | `X-OathNet-Timestamp`                                    |
| `delivery_id` | `X-OathNet-Delivery-Id`                                  |
| `raw_body`    | exact request body bytes                                 |
| signing key   | `HKDF-SHA256(secret, info="oathnet-scanner-signing-v1")` |

The signed message is:

```text theme={null}
<timestamp>.<delivery_id>.<raw_body>
```

The expected header value is:

```text theme={null}
sha256=<hex_hmac_sha256>
```

### Python Receiver Example

```python theme={null}
import hashlib
import hmac

from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.hkdf import HKDF


def _derive_signing_key(secret: str) -> bytes:
    return HKDF(
        algorithm=hashes.SHA256(),
        length=32,
        salt=None,
        info=b"oathnet-scanner-signing-v1",
    ).derive(secret.encode("utf-8"))


def verify_oathnet_signature(headers: dict[str, str], raw_body: bytes, secret: str) -> bool:
    timestamp = headers["x-oathnet-timestamp"]
    delivery_id = headers["x-oathnet-delivery-id"]
    provided = headers["x-oathnet-signature"]

    message = timestamp.encode("utf-8") + b"." + delivery_id.encode("utf-8") + b"." + raw_body
    digest = hmac.new(_derive_signing_key(secret), message, hashlib.sha256).hexdigest()
    expected = f"sha256={digest}"
    return hmac.compare_digest(expected, provided)
```

### Node.js Receiver Example

```typescript theme={null}
import { createHmac, hkdfSync, timingSafeEqual } from "node:crypto";

function verifyOathNetSignature(
  headers: Record<string, string>,
  rawBody: Buffer,
  secret: string
): boolean {
  const timestamp = headers["x-oathnet-timestamp"];
  const deliveryId = headers["x-oathnet-delivery-id"];
  const provided = headers["x-oathnet-signature"];

  const key = hkdfSync(
    "sha256",
    Buffer.from(secret, "utf8"),
    Buffer.alloc(0),
    Buffer.from("oathnet-scanner-signing-v1"),
    32
  );
  const message = Buffer.concat([
    Buffer.from(`${timestamp}.${deliveryId}.`, "utf8"),
    rawBody
  ]);
  const expected = `sha256=${createHmac("sha256", key).update(message).digest("hex")}`;

  return (
    provided.length === expected.length &&
    timingSafeEqual(Buffer.from(provided), Buffer.from(expected))
  );
}
```

For `signed_encrypted`, first verify the signature against the raw encrypted
envelope body. Then decrypt the JSON envelope with AES-256-GCM using
`HKDF-SHA256(secret, info="oathnet-scanner-encryption-v1")`. The envelope
contains `alg`, `iv`, `ciphertext`, and `kid`; `ciphertext` is base64 encoded and
already includes the GCM tag.

<Warning>
  Store the webhook secret immediately. `rotate_webhook_secret` returns the new
  secret once, and later security reads only show a preview. Reject old timestamps
  and store `delivery_id` values you have processed to prevent replay.
</Warning>

## Management Operations

Scanner management is exposed through the public API, but the important product
concepts are simple:

* quota tells you how many scanners the account can still create
* test delivery sends a sample notification without waiting for the schedule
* manual trigger queues a real run for an active scanner
* pause stops scheduling without deleting the scanner
* resume restarts a paused or disabled scanner after the issue is fixed
* run history shows recent execution and notification outcomes
* run detail explains one specific run and its delivery attempts

Use the OpenAPI reference for the exact scanner paths, parameters, statuses, and
playground requests.

## Troubleshooting

### No results found

* scanners only look for newly indexed data after the current baseline
* use regular search endpoints to inspect historical data
* verify that `query_config` contains valid filters for the scanner type
* verify that structured filters target allowed fields for that scanner type

### Not receiving notifications

* run a delivery test before waiting for the hourly schedule
* check the saved webhook security configuration
* inspect run history for delivery failures
* verify the webhook target returns a `2xx` response

### Scanner disabled

* scanners are auto-disabled after repeated failures
* fix the delivery or query issue, then resume the scanner
* check quota and plan access if creation or execution is blocked

## Scanner Management

Scanner management is available today through the public API. This guide explains
the workflow and behavior; use the [OpenAPI reference](/api-reference/overview)
for the exact scanner operations, schemas, and playground requests.
