# Search Query API Reference

Radiant Security exposes a Quickwit-compatible multi-search endpoint that lets you query raw events in Log Management programmatically. Requests are sent as NDJSON payloads over HTTPS and support both search queries (returning individual event records) and aggregation queries (returning counts and bucketed summaries). This reference covers authentication, request structure, query building blocks, response formats, and error handling.

Use the links below to jump to a section:

* [Authentication](#authentication)
* [Endpoint and headers](#endpoint-and-headers)
* [Request body format](#request-body-format)
* [Query building blocks](#query-building-blocks)
* [Search requests](#search-requests)
* [Aggregation requests](#aggregation-requests)
* [Response format](#response-format)
* [Python example](#python-example)
* [Best practices](#best-practices)
* [Error handling](#error-handling)

### Prerequisites

Before using this API, you need:

* A Radiant Security tenant with Log Management enabled.
* A Radiant API bearer token. The token is available in the **Grafana Security Ops - Raw Events** data connector in the Radiant application.
* Working knowledge of REST APIs, cURL or Postman, and JSON.
* Familiarity with Elasticsearch or Quickwit query DSL.

{% hint style="warning" %}
**Important Note:** Store your API token in an environment variable or secrets manager. Never commit tokens to source control.
{% endhint %}

### Authentication

All requests require a Bearer token in the `Authorization` header. Tokens are scoped to your tenant and issued by Radiant Security.

```
Authorization: Bearer ${RADIANT_API_TOKEN}
```

### Endpoint and headers

| Property | Value                                                           |
| -------- | --------------------------------------------------------------- |
| Method   | `POST`                                                          |
| Base URL | `https://plugin.radiantsecurity.ai`                             |
| Path     | `/quickwit-api-proxy/_msearch`                                  |
| Full URL | `https://plugin.radiantsecurity.ai/quickwit-api-proxy/_msearch` |

#### Request headers

<table><thead><tr><th width="183.15234375">Header</th><th width="134.0859375">Required</th><th>Details</th></tr></thead><tbody><tr><td><code>Authorization</code></td><td>Yes</td><td>Bearer token. Include a space between <code>Bearer</code> and the token value: <code>Bearer 1234567890abcdefgh...</code></td></tr><tr><td><code>Content-Type</code></td><td>Yes</td><td>Must be <code>application/x-ndjson</code>.</td></tr><tr><td><code>x-rs-is-byob</code></td><td>Conditional</td><td>Set to <code>true</code> only if your tenant uses a customer-owned <a href="/pages/uXzOMYe4UwaZUAnH3Acu">S3 bucket (BYOB) for Log Management </a>storage. Otherwise, omit this header.</td></tr></tbody></table>

### Request body format

The request body consists of exactly two JSON objects, each on its own line, separated by a single `\n`, with a trailing newline:

```
<header line>   ← msearch metadata
<body line>     ← query, size, sort, and optional aggregations
```

When using cURL, pass the body with `--data-binary` and either a `$'...'` string or a file to preserve newlines exactly.

#### Header line

The header line is the same for every request. The `index` array is intentionally empty - Radiant routes queries by the connector type specified inside the query string.

```json
{"ignore_unavailable": true, "index": [""]}
```

#### Body line fields

<table><thead><tr><th width="211.796875">Field</th><th width="118.1796875">Required</th><th>Details</th></tr></thead><tbody><tr><td><code>query</code></td><td>Yes</td><td>Query DSL object. Typically a <code>bool.filter</code> combining a time range and a query string filter.</td></tr><tr><td><code>size</code></td><td>Yes</td><td>Maximum hits to return. Set to <code>0</code> for aggregation-only queries.</td></tr><tr><td><code>sort</code></td><td>Yes</td><td>Sort descriptors. Always include <code>rs_timestamp</code> with <code>"format": "epoch_nanos_int"</code> and <code>"order": "desc"</code>.</td></tr><tr><td><code>aggs</code> or <code>aggregations</code></td><td>No</td><td>Aggregation definitions for counts, bucketing, or statistics.</td></tr></tbody></table>

### Query building blocks

#### Time range filter

Every query should include a time range on `rs_timestamp`. Timestamps must be ISO 8601 UTC strings.

| Field | Details                                |
| ----- | -------------------------------------- |
| `gte` | Greater than or equal to (start time). |
| `lte` | Less than or equal to (end time).      |

```json
{
  "range": {
    "rs_timestamp": {
      "gte": "2026-02-01T00:00:00Z",
      "lte": "2026-02-08T00:00:00Z"
    }
  }
}
```

#### Query string filter

Use `query_string` to filter by connector type, event class, or any indexed field. Phrase values must be double-quoted.

```json
{
  "query_string": {
    "default_operator": "AND",
    "query": "rs_connectorType:\"okta_system_logs\" AND actor.displayName:\"Eric\""
  }
}
```

Common patterns:

* `rs_connectorType:value` scopes results to a specific connector.
* A bare keyword (for example, `username`) performs a full-text search across all fields.
* `field:value1 OR field:value2` applies OR logic between clauses.

#### Bool query (combining filters)

Wrap multiple filters in `bool.filter` to require all conditions simultaneously:

```json
{
  "bool": {
    "filter": [
      { "range": { "rs_timestamp": { "gte": "...", "lte": "..." } } },
      { "query_string": { "default_operator": "AND", "query": "..." } }
    ]
  }
}
```

#### Sort

The recommended sort returns events in reverse chronological order. The secondary `_doc` sort ensures stable pagination:

```json
[
  { "rs_timestamp": { "format": "epoch_nanos_int", "order": "desc" } },
  { "_doc": { "order": "desc" } }
]
```

### Search requests

Use a search request to retrieve individual event records. Set `size` to the maximum number of hits you want returned (0–1000).

#### Request body

```json
{
  "query": {
    "bool": {
      "filter": [
        {
          "range": {
            "rs_timestamp": {
              "gte": "2026-02-01T00:00:00Z",
              "lte": "2026-02-08T00:00:00Z"
            }
          }
        },
        {
          "query_string": {
            "default_operator": "AND",
            "query": "rs_connectorType:\"cisco_ise\" AND msg_class:\"Certificate\""
          }
        }
      ]
    }
  },
  "size": 5,
  "sort": [
    { "rs_timestamp": { "format": "epoch_nanos_int", "order": "desc" } },
    { "_doc": { "order": "desc" } }
  ]
}
```

#### cURL examples

{% tabs %}
{% tab title="Full-text search" %}
Searches for the keyword `username` across all events and fields within a time window. The `x-rs-is-byob` header indicates that Log Management storage uses a customer-owned S3 bucket. Omit this header if your tenant uses Radiant-managed storage.

{% code overflow="wrap" %}

```bash
curl -X POST \
"https://plugin.radiantsecurity.ai/quickwit-api-proxy/_msearch" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/x-ndjson" \
-H "x-rs-is-byob: true" \
--data-binary $'{"ignore_unavailable": true, "index": [""]}\n{"query":{"bool":{"filter":[{"range":{"rs_timestamp":{"gte":"2026-04-01T00:00:00Z","lte":"2026-04-08T00:00:00Z"}}},{"query_string":{"default_operator":"AND","query":"username"}}]}},"size":5,"sort":[{"rs_timestamp":{"format":"epoch_nanos_int","order":"desc"}},{"_doc":{"order":"desc"}}]}\n'
```

{% endcode %}

{% hint style="info" %}
**Note:** The `\n` character inside `$'...'` produces a literal newline in bash. The trailing `\n` after the body line is required.
{% endhint %}
{% endtab %}

{% tab title="Field:value search" %}
Searches two fields combined with an AND operator. The inner `\"` delimiters require an additional escape (`\\"`) because cURL executes inside a shell. This example uses Radiant-managed storage, so the `x-rs-is-byob` header is omitted.

{% code overflow="wrap" %}

```bash
curl -X POST \
"https://plugin.radiantsecurity.ai/quickwit-api-proxy/_msearch" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/x-ndjson" \
--data-binary $'{"ignore_unavailable": true, "index": [""]}\n{"query":{"bool":{"filter":[{"range":{"rs_timestamp":{"gte":"2026-04-01T00:00:00Z","lte":"2026-04-08T00:00:00Z"}}},{"query_string":{"default_operator":"AND","query":"rs_connectorType:\\"cisco_ise\\" AND msg_class:\\"Passed-Authentication\\""}}]}},"size":5,"sort":[{"rs_timestamp":{"format":"epoch_nanos_int","order":"desc"}},{"_doc":{"order":"desc"}}]}\n'
```

{% endcode %}
{% endtab %}
{% endtabs %}

### Aggregation requests

Use an aggregation request to obtain counts or bucketed summaries without retrieving raw events. Set `size` to `0` to suppress hits.

#### Request body

```json
{
  "query": {
    "bool": {
      "filter": [
        {
          "range": {
            "rs_timestamp": {
              "gte": "2026-02-01T00:00:00Z",
              "lte": "2026-02-08T00:00:00Z"
            }
          }
        },
        {
          "query_string": {
            "default_operator": "AND",
            "query": "rs_connectorType:\"cisco_ise\""
          }
        }
      ]
    }
  },
  "size": 0,
  "sort": [
    { "rs_timestamp": { "format": "epoch_nanos_int", "order": "desc" } }
  ],
  "aggs": {
    "values": {
      "terms": { "field": "msg_class", "size": 20 }
    }
  }
}
```

#### cURL example

```bash
curl -X POST \
  "https://plugin.radiantsecurity.ai/quickwit-api-proxy/_msearch" \
  -H "Authorization: Bearer ${RADIANT_API_TOKEN}" \
  -H "Content-Type: application/x-ndjson" \
  --data-binary @query.ndjson
```

{% hint style="info" %}
**Note:** For complex queries, write the NDJSON payload to a file and pass it with `--data-binary @file.ndjson`. This avoids shell-escaping mistakes.
{% endhint %}

### Response format

All responses return JSON with a top-level `responses` array. Each element corresponds to one body line in the request.

#### Search response

```json
{
  "responses": [
    {
      "hits": {
        "total": 123,
        "hits": [
          {
            "_source": {
              "rs_timestamp": 1770000000000000000,
              "msg_class": "RADIUS",
              "rs_connectorType": "cisco_ise"
            }
          }
        ]
      }
    }
  ]
}
```

<table><thead><tr><th width="247.7421875">Field</th><th>Description</th></tr></thead><tbody><tr><td><code>responses[0].hits.total</code></td><td>Total matched event count. May be an integer or an object of the form <code>{"value": N}</code> — handle both.</td></tr><tr><td><code>responses[0].hits.hits</code></td><td>Array of matched events. Each element contains a <code>_source</code> with the raw event payload.</td></tr><tr><td><code>_source.rs_timestamp</code></td><td>Event timestamp in epoch nanoseconds (integer).</td></tr><tr><td><code>_source.rs_connectorType</code></td><td>Connector that ingested the event (for example, <code>cisco_ise</code>, <code>okta</code>).</td></tr></tbody></table>

#### Aggregation response

```json
{
  "responses": [
    {
      "aggregations": {
        "values": {
          "buckets": [
            { "key": "RADIUS", "doc_count": 312 },
            { "key": "EAP",    "doc_count": 75  }
          ]
        }
      }
    }
  ]
}
```

<table><thead><tr><th width="309.5546875">Field</th><th>Description</th></tr></thead><tbody><tr><td><code>responses[0].aggregations.&#x3C;name></code></td><td>Aggregation results keyed by the name specified in <code>aggs</code>.</td></tr><tr><td><code>buckets[].key</code></td><td>The field value for this bucket.</td></tr><tr><td><code>buckets[].doc_count</code></td><td>Number of events in this bucket within the query window.</td></tr></tbody></table>

### Python example

This example assumes an environment variable `RADIANT_API_TOKEN` set to your bearer token. The `x-rs-is-byob` header is set to `true`; remove it if your tenant uses Radiant-managed storage.

```python
import os
import json
import requests

API_URL = "https://plugin.radiantsecurity.ai/quickwit-api-proxy/_msearch"
TOKEN   = os.environ["RADIANT_API_TOKEN"]

header = {"ignore_unavailable": True, "index": [""]}
body = {
    "query": {
        "bool": {
            "filter": [
                {"range": {"rs_timestamp": {
                    "gte": "2026-04-01T00:00:00Z",
                    "lte": "2026-04-08T00:00:00Z",
                }}},
                {"query_string": {
                    "default_operator": "AND",
                    "query": "*",
                }},
            ]
        }
    },
    "size": 10,
    "sort": [{"rs_timestamp": {"format": "epoch_nanos_int", "order": "desc"}}],
}

ndjson = json.dumps(header) + "\n" + json.dumps(body) + "\n"

resp = requests.post(
    API_URL,
    headers={
        "Authorization": f"Bearer {TOKEN}",
        "Content-Type": "application/x-ndjson",
        "x-rs-is-byob": "true",
    },
    data=ndjson.encode(),
)
resp.raise_for_status()

hits = resp.json()["responses"][0]["hits"]["hits"]
for hit in hits:
    print(hit["_source"])
```

### Best practices

* Always include a time range. Queries without an `rs_timestamp` range scan the entire dataset, run slowly, consume excess capacity, and may time out without returning results.
* Scope by connector. Add `rs_connectorType:<name>` to limit results to the data source you need.
* Use exact matches for event types. Phrase-quote values such as `msg_class:"Certificate"` to avoid partial matches.
* Set `size: 0` for aggregation-only queries. Returning hits when you only need counts adds latency and wastes bandwidth.
* Pass complex payloads from a file. Use `--data-binary @file.ndjson` to avoid shell-escaping errors.
* Validate timestamp format. Range filter timestamps must be ISO 8601 UTC strings (`YYYY-MM-DDTHH:MM:SSZ`). Epoch integers are not accepted in the range filter.
* Secure your API token. Never commit tokens to source control. Use environment variables or a secrets manager.

### Error handling

| Status                      | Meaning                                                                                                  |
| --------------------------- | -------------------------------------------------------------------------------------------------------- |
| `200 OK`                    | Request succeeded. Inspect `responses[0]` for results or query-level errors.                             |
| `400 Bad Request`           | Malformed NDJSON or invalid query DSL. Confirm that exactly two lines are present and all JSON is valid. |
| `401 Unauthorized`          | Missing or invalid bearer token.                                                                         |
| `403 Forbidden`             | Token is valid but lacks permission for this resource.                                                   |
| `429 Too Many Requests`     | Rate limit exceeded. Back off and retry with exponential delay.                                          |
| `500 Internal Server Error` | Backend error. Retry with exponential backoff and contact support if the issue persists.                 |

{% hint style="warning" %}
**Important Note:** Always check the HTTP status code before parsing the response body. A `200` response can still contain a query-level `error` object inside `responses[0]` if the query DSL was accepted but failed during execution.
{% endhint %}

### Support

For questions, issues, or access requests, contact your Radiant Security account team or open a support ticket through the Radiant Security portal.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://help.radiantsecurity.ai/log-management/log-search-and-query/search-query-api-reference.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
