Skip to main content
Version: v12

BQL API

The BQL API provides programmatic access to the Brinqa data warehouse using BQL (Brinqa Query Language). It replaces the legacy GraphQL API with a simpler, more flexible interface that leverages the same query engine used by the Brinqa UI.

Authentication​

The BQL API uses API tokens for authentication. See API Token Authentication for instructions on generating, using, and managing API tokens.

note

The BQL API only accepts API token authentication. Session-based or Bearer token authentication is not accepted for BQL API endpoints.

Querying data​

The BQL API uses an asynchronous, two-step query workflow:

  1. Start a query (POST). Returns a polling URL.
  2. Fetch results (GET). Poll until results are ready.

Start a query​

POST /v1/api/bql

Headers:

Content-Type: application/json
Authorization: ApiKey <your-api-token>

Request body:

{
"query": "FIND Vulnerability WHERE riskScore > 8",
"returningFields": ["id", "name", "riskScore"],
"orderBy": ["riskScore DESC"],
"limit": 100
}

Response: 202 Accepted

The response body is empty. The response headers contain:

HeaderDescription
LocationThe URL to poll for results (e.g., /v1/api/bql/eyJqb2J...).
Retry-AfterRecommended wait time in seconds before polling.

Fetch results​

GET /v1/api/bql/{token}

Headers:

Authorization: ApiKey <your-api-token>

Poll the URL from the Location header. The response depends on the query status:

While processing (202 Accepted)​

{
"status": "processing",
"results": []
}

Headers include Location and Retry-After, which indicates when to poll again.

When complete (200 OK)​

{
"status": "completed",
"state": "success",
"message": "Query completed successfully.",
"results": [
{
"id": 1847261953084,
"name": "CVE-2024-1234",
"riskScore": 9.8
},
{
"id": 1847261953085,
"name": "CVE-2024-5678",
"riskScore": 9.1
}
],
"totalRows": 1458,
"pageRows": 312,
"cursor": "eyJwYWdl..."
}
FieldTypeDescription
statusstring"processing" or "completed".
statestring"success" or "failed" (only present when status is "completed").
messagestringDescriptive message about the result.
resultsarrayArray of result objects. Each object contains the requested fields. Up to 10,000 rows are returned per page.
totalRowsintegerTotal number of rows matched by the query across all pages. May be null while the total is still being computed.
pageRowsintegerNumber of rows returned in the current page (i.e., the size of the results array).
cursorstringOpaque token for fetching the next page. null if there are no more results.

Fetching additional pages: If the cursor is not null, the query returned more results than fit in a single page. To retrieve the next page:

  1. Read the Location header from the response. It contains the URL for the next page (e.g., /v1/api/bql/eyJuZXh0...).
  2. Make a GET request to that URL with the same Authorization header.
  3. The response has the same format: a new results array and a new cursor. If the cursor is null, you have reached the last page.

The response also includes a Link header (<next-page-url>; rel="next") that you can use as an alternative to the Location header.

When failed​

{
"status": "completed",
"state": "failed",
"message": "Query failed (invalidQuery): Syntax error in BQL expression.",
"results": []
}

Complete workflow example​

This example starts a query, waits for results, and follows pagination to collect all pages.

BASE_URL="https://brinqa.example.com"
API_TOKEN="ApiKey brq_AbCdEfGh.xYz123..."

# Step 1: Start the query
HEADERS=$(curl -s -D - -o /dev/null -X POST "$BASE_URL/v1/api/bql" \
-H 'Content-Type: application/json' \
-H "Authorization: $API_TOKEN" \
-d '{
"query": "FIND Vulnerability WHERE riskScore > 7",
"returningFields": ["id", "name", "riskScore", "status"],
"orderBy": ["riskScore DESC"]
}')

LOCATION=$(echo "$HEADERS" | grep -i '^Location:' | awk '{print $2}' | tr -d '\r')
RETRY_AFTER=$(echo "$HEADERS" | grep -i '^Retry-After:' | awk '{print $2}' | tr -d '\r')

# Step 2: Poll until the query completes, then paginate through all results
while true; do
sleep "$RETRY_AFTER"

RESPONSE=$(curl -s -D /tmp/bql_headers.txt "$BASE_URL$LOCATION" \
-H "Authorization: $API_TOKEN")

STATUS=$(echo "$RESPONSE" | jq -r '.status')

# Still running -- wait and poll the same URL again
if [ "$STATUS" = "processing" ]; then
RETRY_AFTER=$(grep -i '^Retry-After:' /tmp/bql_headers.txt | awk '{print $2}' | tr -d '\r')
continue
fi

# Query finished -- check for errors
STATE=$(echo "$RESPONSE" | jq -r '.state')
if [ "$STATE" = "failed" ]; then
echo "Query failed: $(echo "$RESPONSE" | jq -r '.message')"
exit 1
fi

# Print this page's results
echo "$RESPONSE" | jq '.results'

# If cursor is null, this was the last page
CURSOR=$(echo "$RESPONSE" | jq -r '.cursor // empty')
if [ -z "$CURSOR" ]; then
echo "All pages fetched."
break
fi

# Follow the Location header to the next page
LOCATION=$(grep -i '^Location:' /tmp/bql_headers.txt | awk '{print $2}' | tr -d '\r')
RETRY_AFTER=0
done

Request modes​

The BQL API supports two distinct modes for requesting data. The mode is determined by whether you include the returningFields parameter in your request.

Analytics mode​

In this mode, the BQL query string controls which fields are returned, sorting, and row limits. Use full BQL syntax including RETURN, ORDER BY, and LIMIT clauses directly in the query field.

{
"query": "FIND Vulnerability RETURN id, name, riskScore ORDER BY name LIMIT 100"
}

This mode works the same way as BQL queries in the Brinqa UI (e.g., reports and dashboards).

Projection mode​

In this mode, the query field contains only the entity, relationships, and conditions (the FIND ... WHERE ... clauses). The output is controlled by separate parameters: returningFields, orderBy, and limit.

{
"query": "FIND Vulnerability WHERE riskScore > 8",
"returningFields": ["id", "name", "riskScore"],
"orderBy": ["riskScore DESC"],
"limit": 50
}

This mode enables features not available in analytics mode, such as nested relationship projections and wildcards. See Returning fields in detail for all options.

note

When a value for the returningFields is provided, the projection fields take precedence over any RETURN, ORDER BY, or LIMIT clauses in the query string.

How each mode handles relationships​

The two modes produce fundamentally different result structures when querying related entities.

Analytics mode: flattened results​

In analytics mode, you use THAT clauses to traverse relationships, and related entity attributes are flattened into the same result row. The field names in the result preserve the alias prefix from the query (e.g., vd.displayName).

For one-to-one relationships, the related attributes appear as additional fields in each row:

{
"query": "FIND Vulnerability AS v THAT IS VulnerabilityDefinition AS vd RETURN v.id, v.riskScore, vd.displayName, vd.description LIMIT 10"
}

Example response (results array):

[
{ "v.id": 1847261953084, "v.riskScore": 9.8, "vd.displayName": "CVE-2024-1234", "vd.description": "SQL injection in login form" },
{ "v.id": 1847261953085, "v.riskScore": 8.5, "vd.displayName": "CVE-2024-5678", "vd.description": "XSS in search field" }
]

For one-to-many relationships, each related entity produces a separate row. The main entity's attributes are repeated for each related entity:

{
"query": "FIND Vulnerability AS v THAT HAS Host AS h RETURN v.id, v.riskScore, h.displayName, h.ipAddresses LIMIT 10"
}

Example response (results array):

[
{ "v.id": 1847261953084, "v.riskScore": 9.8, "h.displayName": "prod-web-01.us-east.example.com", "h.ipAddresses": ["10.0.0.1", "10.0.0.2"] },
{ "v.id": 1847261953084, "v.riskScore": 9.8, "h.displayName": "prod-db-02.us-west.example.com", "h.ipAddresses": ["10.0.1.1"] },
{ "v.id": 1847261953085, "v.riskScore": 8.5, "h.displayName": "prod-web-01.us-east.example.com", "h.ipAddresses": ["10.0.0.1", "10.0.0.2"] }
]

This is similar to how a SQL JOIN works. Vulnerability 1847261953084 appears in two rows because it has two hosts.

Projection mode: nested results​

In projection mode, related entities are returned as nested objects within a single row. One-to-many relationships produce an array, and one-to-one relationships produce a single object.

{
"query": "FIND Vulnerability",
"returningFields": ["id", "riskScore", "targets(displayName, ipAddresses)"]
}

Example response (results array):

[
{
"id": 1847261953084,
"riskScore": 9.8,
"targets": [
{ "displayName": "prod-web-01.us-east.example.com", "ipAddresses": ["10.0.0.1", "10.0.0.2"] },
{ "displayName": "prod-db-02.us-west.example.com", "ipAddresses": ["10.0.1.1"] }
]
},
{
"id": 1847261953085,
"riskScore": 8.5,
"targets": [
{ "displayName": "prod-web-01.us-east.example.com", "ipAddresses": ["10.0.0.1", "10.0.0.2"] }
]
}
]

Each vulnerability appears once, with all its targets grouped in an array.

Request parameters​

ParameterTypeRequiredDescription
querystringYesThe BQL query to execute. In analytics mode, include RETURN/ORDER BY/LIMIT clauses. In projection mode, include only FIND ... WHERE ....
returningFieldsarray of stringsNoControls which attributes appear in each result row. When provided, the API uses projection mode. See Returning fields in detail.
orderByarray of stringsNoSort order. Each entry is a field name optionally followed by ASC (ascending, default) or DESC (descending). Example: ["riskScore DESC", "name ASC"].
limitintegerNoMaximum number of rows to return. If not specified, all matching rows are returned, paginated in pages of up to 10,000 rows each.
appstringNoThe application context for the query. Used when the Brinqa instance has multiple apps configured.

Returning fields in detail​

The returningFields parameter provides fine-grained control over which data is included in each result row. Its behavior depends on the value provided.

Analytics mode (returningFields omitted)​

When returningFields is not included in the request, the API operates in analytics mode. The RETURN clause in your BQL query string determines which columns are returned. No nested relationship data is available in this mode.

{
"query": "FIND Vulnerability RETURN id, name, riskScore ORDER BY name LIMIT 100"
}

Default projection (empty array)​

When returningFields is an empty array [], the API returns only the id and the display attribute of the main entity. The display attribute is defined in each Data Model (e.g., displayName for Vulnerability).

{
"query": "FIND Vulnerability WHERE riskScore > 8",
"returningFields": []
}

Explicit fields​

List specific attribute names to include only those fields in the results.

{
"query": "FIND Vulnerability",
"returningFields": ["id", "name", "riskScore", "status"]
}

Wildcard​

Use "*" to return every persisted attribute of the main entity. Relationships are automatically expanded with their id and display attribute.

{
"query": "FIND Vulnerability",
"returningFields": ["*"]
}

Relationship projections​

You can include data from related entities using nested projection syntax. In the examples below, targets is a relationship attribute on Vulnerability that points to related assets.

Bare relationship name​

Using just the relationship name returns each related entity's id and display attribute.

{
"query": "FIND Vulnerability",
"returningFields": ["id", "name", "targets"]
}

Nested projection with specific fields​

Specify exactly which attributes of the related entity to return.

{
"query": "FIND Vulnerability",
"returningFields": ["id", "name", "targets(ip, hostname)"]
}

Nested wildcard​

Use * inside the projection to return all simple (non-relationship) attributes of the related entity.

{
"query": "FIND Vulnerability",
"returningFields": ["id", "type(*)"]
}

Response shape for relationships​

Relationship typeJSON value
One-to-many (e.g., Vulnerability -> targets)Array of objects: "targets": [{"ip": "10.0.0.1"}, ...]
One-to-one (e.g., Vulnerability -> type)Single object or null: "type": {"id": 1512748391002, "name": "SQL Injection"}

Limitation​

Nested-inside-nested projections are not supported. You cannot request a relationship attribute inside a projection where that attribute is itself a relationship (e.g., targets(id, type) where type is a relationship). This produces an error.

Combining modes​

You can freely combine explicit fields, bare relationships, nested projections, and wildcards:

{
"query": "FIND Vulnerability WHERE status = \"open\"",
"returningFields": ["id", "name", "riskScore", "targets(ip, hostname)", "type(*)"],
"orderBy": ["riskScore DESC"],
"limit": 50
}

Pagination​

Results are delivered in pages of up to 10,000 rows each. When more results are available, the response includes a cursor field and a Location header with the URL for the next page.

Pagination flow:

  1. Start your query with POST /v1/api/bql. You receive a Location header.
  2. Fetch the first page with GET /v1/api/bql/{token}.
  3. If the response contains a cursor value (non-null), more results are available. The response also includes:
    • A Location header with the URL for the next page.
    • A Link header: <next-page-url>; rel="next".
  4. Fetch the next page by following the Location or Link header URL.
  5. Repeat until cursor is null, indicating no more results.

ETag support:

Each completed page response includes an ETag header. You can include an If-Match header in subsequent fetch requests to verify that results have not changed. If the results have changed, the API returns 412 Precondition Failed.

# Fetch first page
RESPONSE=$(curl -s -D /tmp/bql_headers.txt 'https://brinqa.example.com/v1/api/bql/eyJqb2J...' \
-H 'Authorization: ApiKey brq_AbCdEfGh.xYz123...')

# Extract ETag from response headers
ETAG=$(grep -i 'ETag:' /tmp/bql_headers.txt | awk '{print $2}' | tr -d '\r')

# Fetch next page with ETag validation
curl -s 'https://brinqa.example.com/v1/api/bql/eyJuZXh0...' \
-H 'Authorization: ApiKey brq_AbCdEfGh.xYz123...' \
-H "If-Match: $ETAG"

Rate limiting​

The BQL API enforces rate limits to ensure fair usage. Separate rate limits apply to:

  • Starting queries (POST /v1/api/bql)
  • Fetching results (GET /v1/api/bql/{token})

When you exceed the rate limit, the API returns:

429 Too Many Requests

{
"status": 429,
"error": "Too Many Requests",
"message": "Rate limit exceeded"
}

The response includes a Retry-After header indicating how many seconds to wait before retrying.

Best practices:

  • Respect the Retry-After header values returned in both 202 Accepted and 429 responses.
  • Avoid sending requests faster than the recommended polling interval.
  • Use exponential backoff when receiving 429 responses.

Error handling​

HTTP StatusMeaningCommon cause
202 AcceptedQuery submitted or still processing.Normal. Poll the Location URL.
200 OKResults ready (check state for success/failure).Normal response.
400 Bad RequestInvalid request body.The required query field is missing or blank. Returned by POST /v1/api/bql.
422 Unprocessable EntityMalformed request body.The request body is not valid JSON. Returned by POST /v1/api/bql.
401 UnauthorizedAuthentication failed.Missing, invalid, expired, or revoked API token.
404 Not Found / 406 Not AcceptableUnknown or invalid result token.The token in GET /v1/api/bql/{token} does not identify a known query result (expired, already consumed, or never issued).
412 Precondition FailedETag mismatch.Results changed between fetches (when using If-Match).
429 Too Many RequestsRate limit exceeded.Too many requests in a short period. Wait and retry.

Request-shape errors (a malformed body or an unknown result token) are returned as the HTTP client errors listed above. Query-level errors, by contrast, are returned in the response body with state: "failed" and a descriptive message, not as HTTP errors:

{
"status": "completed",
"state": "failed",
"message": "Query failed (invalidQuery): Unrecognized entity type 'Vuln'.",
"results": []
}

Quick reference​

ItemValue
Start queryPOST /v1/api/bql
Fetch resultsGET /v1/api/bql/{token}
Auth headerAuthorization: ApiKey <token>
Token formatbrq_<id>.<secret>
Token expiry options1, 3, 6, 12 months, or never
Max rows per page10,000
Manage tokensAccount icon > API Tokens

For specific examples of how to use the BQL API to retrieve different datasets, see the following articles:

Beyond querying data with BQL, the Brinqa API also lets you act on records and workflows: