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
Tip

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 records
  • assignees: [UUID!] — filter by assigned user IDs
  • search: String — free-text search on name/description

Activities (issues, tasks):

  • closed: Boolean — open or closed activities
  • controls: [UUID!] — activities linked to specific controls
  • assets: [UUID!] — activities linked to specific assets
  • status: [ActivityDueStatus!]NOT_DUE_SOON, DUE_SOON, OVERDUE, NOT_SET

Issues only:

  • priority: [Priority!]LOW, MEDIUM, HIGH
  • type: [IssueType!] — e.g. AUDIT_FINDING, INCIDENT, CONTROL_GAP

Risks and controls:

  • frameworks: [UUID!] — linked to specific frameworks
  • references: [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