API Reference
Querying data
All list queries in the Tidal Control API follow the same patterns for pagination, filtering, and sorting. Learn them once and they apply everywhere.
Request structure
Every GraphQL request is a POST to https://portal.tidalcontrol.com/graphql with a JSON body:
{
"query": "...",
"variables": { ... }
}
Always use variables instead of inlining values into the query string — it keeps queries readable and avoids injection issues.
Pagination
All *_paged queries use cursor-based pagination following the Relay Connection spec. Request a page with first (page size) and after (cursor from the previous page):
query ExportRisks($first: Int, $after: String) {
risks_paged(first: $first, after: $after) {
edges {
node {
id
name
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
Variables:
{ "first": 50, "after": null }
When pageInfo.hasNextPage is true, pass pageInfo.endCursor as after in the next request.
Fetching all pages (Python)
def fetch_all(graphql_fn, query, page_size=50):
"""Fetch all pages of a paged query. graphql_fn(query, variables) must return data."""
results = []
cursor = None
while True:
data = graphql_fn(query, {"first": page_size, "after": cursor})
# Extract the first (and only) key from the response
key = next(iter(data))
page = data[key]
results.extend(edge["node"] for edge in page["edges"])
if not page["pageInfo"]["hasNextPage"]:
break
cursor = page["pageInfo"]["endCursor"]
return results
A page size of 50 is a good default. You can go higher, but very large pages may increase response time.
Filtering
Pass a filter argument to narrow results. Each entity has its own filter type with relevant fields. All filter fields are optional — omit any field you don't need.
Example — fetch only open, high-priority issues:
query FilteredIssues($filter: IssueFilter) {
issues_paged(first: 50, filter: $filter) {
edges {
node {
id
name
priority
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
Variables:
{
"filter": {
"closed": false,
"priority": ["HIGH"]
}
}
Common filter fields
Available on most entities:
archived: Boolean— include or exclude archived recordsassignees: [UUID!]— filter by assigned user IDssearch: String— free-text search on name/description
Activities (issues, tasks):
closed: Boolean— open or closed activitiescontrols: [UUID!]— activities linked to specific controlsassets: [UUID!]— activities linked to specific assetsstatus: [ActivityDueStatus!]—NOT_DUE_SOON,DUE_SOON,OVERDUE,NOT_SET
Issues only:
priority: [Priority!]—LOW,MEDIUM,HIGHtype: [IssueType!]— e.g.AUDIT_FINDING,INCIDENT,CONTROL_GAP
Risks and controls:
frameworks: [UUID!]— linked to specific frameworksreferences: [UUID!]— linked to specific framework references
Sorting
Pass a sort array to control the order of results. Each sort item specifies a field name and an optional direction (ASC or DESC):
query SortedRisks {
risks_paged(
first: 50,
sort: [{ field: "sequenceId", direction: ASC }]
) {
edges {
node {
sequenceId
name
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
You can pass multiple sort fields. Common sortable fields include name, sequenceId, createdDate, notBefore, and expires.
Counting records
Use *_count queries to get a total count without fetching all data:
query {
risks_count
controls_count(filter: { archived: false })
issues_count(filter: { closed: false, priority: ["HIGH"] })
}
Error handling
A successful response always has HTTP status 200, even when the GraphQL query itself has errors. Check the errors array in the response body:
{
"data": null,
"errors": [
{
"message": "Unauthorized",
"locations": [{ "line": 1, "column": 1 }]
}
]
}
Common errors:
- Unauthorized — your token is missing or expired; re-authenticate
- Field does not exist — check the field name against the schema
- Variable … expected type … — a filter variable has the wrong type
- Previous
- Authentication
- Next
- Exporting risks