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.
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:
- Start a query (
POST). Returns a polling URL. - 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:
| Header | Description |
|---|---|
Location | The URL to poll for results (e.g., /v1/api/bql/eyJqb2J...). |
Retry-After | Recommended 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..."
}
| Field | Type | Description |
|---|---|---|
status | string | "processing" or "completed". |
state | string | "success" or "failed" (only present when status is "completed"). |
message | string | Descriptive message about the result. |
results | array | Array of result objects. Each object contains the requested fields. Up to 10,000 rows are returned per page. |
totalRows | integer | Total number of rows matched by the query across all pages. May be null while the total is still being computed. |
pageRows | integer | Number of rows returned in the current page (i.e., the size of the results array). |
cursor | string | Opaque 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:
- Read the
Locationheader from the response. It contains the URL for the next page (e.g.,/v1/api/bql/eyJuZXh0...). - Make a
GETrequest to that URL with the sameAuthorizationheader. - The response has the same format: a new
resultsarray and a newcursor. If thecursorisnull, 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.
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
| Parameter | Type | Required | Description |
|---|---|---|---|
query | string | Yes | The BQL query to execute. In analytics mode, include RETURN/ORDER BY/LIMIT clauses. In projection mode, include only FIND ... WHERE .... |
returningFields | array of strings | No | Controls which attributes appear in each result row. When provided, the API uses projection mode. See Returning fields in detail. |
orderBy | array of strings | No | Sort order. Each entry is a field name optionally followed by ASC (ascending, default) or DESC (descending). Example: ["riskScore DESC", "name ASC"]. |
limit | integer | No | Maximum number of rows to return. If not specified, all matching rows are returned, paginated in pages of up to 10,000 rows each. |
app | string | No | The 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 type | JSON 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:
- Start your query with
POST /v1/api/bql. You receive aLocationheader. - Fetch the first page with
GET /v1/api/bql/{token}. - If the response contains a
cursorvalue (non-null), more results are available. The response also includes:- A
Locationheader with the URL for the next page. - A
Linkheader:<next-page-url>; rel="next".
- A
- Fetch the next page by following the
LocationorLinkheader URL. - Repeat until
cursorisnull, 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-Afterheader values returned in both202 Acceptedand429responses. - Avoid sending requests faster than the recommended polling interval.
- Use exponential backoff when receiving
429responses.
Error handling
| HTTP Status | Meaning | Common cause |
|---|---|---|
202 Accepted | Query submitted or still processing. | Normal. Poll the Location URL. |
200 OK | Results ready (check state for success/failure). | Normal response. |
400 Bad Request | Invalid request body. | The required query field is missing or blank. Returned by POST /v1/api/bql. |
422 Unprocessable Entity | Malformed request body. | The request body is not valid JSON. Returned by POST /v1/api/bql. |
401 Unauthorized | Authentication failed. | Missing, invalid, expired, or revoked API token. |
404 Not Found / 406 Not Acceptable | Unknown 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 Failed | ETag mismatch. | Results changed between fetches (when using If-Match). |
429 Too Many Requests | Rate 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
| Item | Value |
|---|---|
| Start query | POST /v1/api/bql |
| Fetch results | GET /v1/api/bql/{token} |
| Auth header | Authorization: ApiKey <token> |
| Token format | brq_<id>.<secret> |
| Token expiry options | 1, 3, 6, 12 months, or never |
| Max rows per page | 10,000 |
| Manage tokens | Account icon > API Tokens |
For specific examples of how to use the BQL API to retrieve different datasets, see the following articles:
- Retrieve active assets
- Retrieve new findings
- Query for active and public hosts
- Filter for open and assigned tickets
- Retrieve critical vulnerability details
Related APIs
Beyond querying data with BQL, the Brinqa API also lets you act on records and workflows:
- Comments API: add, list, update, delete, and search comments on records of any data model.
- Flow and Automation Management API: list API-accessible flows, then launch, monitor, resume, and cancel flows and automations.