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. API tokens are tied to a specific user account and inherit that user's access control permissions (ACL/RBAC). This means the API token can only access data that the associated user is authorized to see.
The BQL API only accepts API token authentication. Session-based or Bearer token authentication is not accepted for BQL API endpoints.
Generate an API token
To generate an API token, follow these steps in the Brinqa Platform UI:
- Click the account icon (person circle) in the top navigation bar.
- Select API Tokens from the dropdown menu.
- On the API Tokens page, click the + (create) button in the header.
- In the Generate API Token dialog, fill in:
- Token name (required): A descriptive name to identify this token's purpose (e.g., SOAR Vulnerability Export). Max 255 characters. Letters, numbers, spaces, hyphens, underscores, and periods are allowed.
- Expiration: Select one of: 1 Month, 3 Months, 6 Months, 12 Months, or Never expires.
- Click Generate. The raw token (starting with
brq_) is displayed once.
Copy and store the token securely immediately. It is not shown again. If lost, you must revoke the token and generate a new one.
The API Tokens page also lets you view all your tokens with their status, creation date, expiration date, and last used date.
Token creation restrictions
- Non-administrator users can only create a new token if they have no active tokens, or if their current active token is within 30 days of expiration.
- System administrators can create tokens at any time.
Use your API token
Include the API token in the Authorization header using the ApiKey scheme:
Authorization: ApiKey brq_AbCdEfGh.xYz123...
Example with curl:
curl -X POST 'https://<your-brinqa-instance>/v1/api/bql' \
-H 'Content-Type: application/json' \
-H 'Authorization: ApiKey brq_AbCdEfGh.xYz123...' \
-d '{
"query": "FIND Vulnerability",
"returningFields": ["id", "name"],
"limit": 10
}'
Token lifecycle
| Status | Description |
|---|---|
| Active | The token can be used for authentication. |
| Inactive | The token has been manually revoked and cannot be used. It can be reinstated. |
| Expired | The token has passed its expiration date. It cannot be used or reinstated. |
- Tokens are automatically marked as expired when used after their expiration date.
- An API token's permissions are inherited from the user account for which it was generated. If the user's permissions change, the token reflects the current permissions.
Manage tokens
On the API Tokens page (account icon > API Tokens), you can manage your existing tokens:
- Revoke: Hover over a token row and click the Revoke action icon. This sets the token status to Inactive and it can no longer be used for authentication.
- Reinstate: Hover over a previously revoked token row and click the Reinstate action icon. This reactivates the token, setting its status back to Active. Expired tokens cannot be reinstated.
- Delete: Hover over a token row and click the Delete action icon to permanently remove the token.
Users can revoke, reinstate, and delete their own tokens. System administrators can revoke, reinstate, and delete tokens for any user.
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. |
401 Unauthorized | Authentication failed. | Missing, invalid, expired, or revoked API token. |
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. |
Query-level errors 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_... |
| 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: