> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ctrl-hub.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Get form submission stats

> The aggregate view of an organisation's form submissions: how many there are, how
many fell in a reporting window, how those split by workflow status, and the same
split bucketed over time for a chart.

Everything windowed is computed from the submissions the same filter would list, so
a figure here and a page of submissions under the same filter describe the same
set. The figures are counted under the caller's own grants, so a principal whose
grants reach part of an organisation is told about that part.

It exists so a dashboard can ask for counts rather than records. Assembling the
same figures client side means reading every submission in the window over the
network to count it, which is megabytes of payload and a page-by-page walk to
arrive at a few dozen integers.

The activity series carries every bucket in the reporting window, the empty ones
included, so a consumer renders it as it arrives rather than working out for itself
which buckets held nothing. That is why the window is bounded against the bucket
width: a combination producing more than 1000 buckets is refused with a 400.

A sibling collection rather than a sub-resource of a submission, because the
figures belong to the organisation rather than to any one submission. The
organisation is a filter term rather than a path segment, so that everything
selecting the submissions travels in one parameter.

Served by a hand-written handler rather than a generated one, because the resource
is computed rather than stored.




## OpenAPI

````yaml /api-reference/openapi.yaml get /v3/form-submission-stats
openapi: 3.1.0
info:
  contact:
    email: support@ctrl-hub.com
    name: Ctrl Hub
    url: https://www.ctrl-hub.com
  description: >
    Ctrl Hub is the all-in-one platform for high-risk industries like utilities,
    construction, infrastructure, and renewables. We help teams manage
    everything from risk assessments and HAVS exposure to vehicle and equipment
    checks, with a guaranteed minimum of 200% ROI.
  license:
    name: MIT License
    url: https://opensource.org/licenses/MIT
  summary: An API for managing your compliance and risk posture
  termsOfService: https://www.ctrl-hub.com/terms-conditions
  title: Ctrl Hub
  version: 1.0.0
servers:
  - description: Production
    url: https://api.ctrl-hub.com
  - description: Staging
    url: https://api.ctrl-hub.dev
  - description: Development
    url: https://api.ctrl-hub.run
security: []
tags:
  - description: |
      Actions are follow-ups assigned to users and teams, produced manually or
      by domain producers such as the data-capture workflow runner.
    name: Actions
  - description: |
      Audit events are the events that are logged by the system.
    name: Audit Events
  - description: |
      View the platform's health and availability.
    name: Status
  - description: >
      User-owned dashboards composed of cards on a fixed-slot bento layout.
      Cards come from a per-domain registry; the API stores their config as
      opaque JSON.
    name: Dashboards
  - description: >
      A record of documents the platform generated and handed over, retained as
      evidence of what

      was issued. `subject_type` says what kind of records an export covers, and
      it is what decides

      how the request is authorised: there is no export permission of its own,
      so if you can see the

      records you can export them.
    name: Exports
  - description: >
      Scheduled delivery of a saved query's results, as a CSV attachment or a
      summary email. Every run is retained with the file it sent, so what a
      recipient received stays retrievable. A report runs as the person who
      created it, under their own access.
    name: Reports
  - description: |
      Manage appointments for work to be carried out with your customers
    name: Customer Appointments
  - description: |
      Manage interactions you have with your customers
    name: Customer Interactions
  - description: |
      Manage accounts for your customers
    name: Customer Accounts and Contacts
  - description: |
      Qualifications are the skills and knowledge that an organisation requires.
    name: Qualifications
  - description: |
      Workflows allow you to automate your processes.
    name: Workflows
  - description: |
      Manage documents
    name: Documents
  - description: |
      Manage documents
    name: Folders
  - description: |
      Manage documents
    name: Document Reviews
  - description: |
      Manage feature configurations for an organisation.
    name: Feature Configurations
  - description: |
      Equipment are the physical assets that an organisation manages.
    name: Equipment
  - description: |
      The central cost register: time-versioned rate codes for material, labour,
      contractor and miscellaneous spend, used to cost work against the job
      hierarchy.
    name: Costs
  - description: >
      Locations are places an organisation manages, optionally classified by a
      location type.
    name: Locations
  - description: >
      Bulk loads of locations from an uploaded CSV. An import matches rows on
      the organisation's own

      `reference`, so re-importing a corrected file updates what is already
      there rather than

      doubling the population, and the import record is the audit trail for the
      whole load: the bulk

      write path deliberately emits no per-row events.
    name: Location Imports
  - description: |
      Manage your forms and their schemas
    name: Forms, Schemas and Categories
  - description: |
      Create and view form submissions
    name: Submissions
  - description: |
      View the roles available in the system.
    name: IAM Roles
  - description: >
      IAM role groups can be assigned to principals to manage authorisation
      centrally.
    name: IAM Role Groups
  - description: |
      Manage service accounts which can access the API programmatically.
    name: Service Accounts
  - description: |
      Manage bridges between organisations.
    name: Bridges
  - description: |
      Manage settings for an organisation.
    name: Settings
  - description: |
      Manage teams within an organisation.
    name: Teams
  - description: |
      Manage job roles within an organisation.
    name: Job Roles
  - description: |
      Manage users and accounts.
    name: Users
  - description: |
      Invite and manage invitations to organisations.
    name: Invitations
  - description: >
      IAM grants are the asignment of roles or permissions to principals to
      manage resource access.
    name: IAM Grants
  - description: |
      View the permissions available in the system.
    name: IAM Permissions
  - description: |
      SSO providers are the identity providers for an organisation.
    name: SSO Providers
  - description: |
      Whoami returns information about the currently authenticated principal.
    name: Whoami
  - description: |
      Your personal inbox. A notification is the thing you receive: an envelope
      that carries what happened to one or more channels. Email is one of those
      channels, the inbox is another, and this is where the ones delivered to
      your inbox are listed, read, saved and archived.
    name: Notifications
  - description: |
      Manage your images
    name: Images
  - description: >
      AI agent personas that synthesise data into role-specific intelligent
      briefings.
    name: Agents
  - description: |
      Briefings generated by AI agents, including reasoning traces.
    name: Briefings
  - description: >
      Organisations are the center point for most resources in the platform.
      Most other endpoints are subresources of an organisation.
    name: Organisations
  - description: |
      Permits managements, integrated with street manager.
    name: Permits
  - description: |
      Projects manage your work and governance.
    name: Projects
  - description: >
      Import templates allow users to save and reuse their CSV importer
      configuration as named templates.
    name: Import Templates
  - description: |
      Properties are the physical locations.
    name: Properties
  - description: |
      Search across schemes, work orders, and operations.
    name: Search
  - description: |
      Provides the API specification in JSON and YAML formats
    name: Specifications
  - description: |
      Streets are the physical roads.
    name: Streets
  - description: |
      Integration with street manager
    name: Street Manager
  - description: |
      Vehicles are the physical vehicles that an organisation manages.
    name: Vehicles
  - description: >
      Scheme contracts (also known as regions) group schemes allocated from the
      network to a contractor.
    name: Scheme Contracts
  - description: >
      Scheme shares allow you to share your schemes with other organisations
      across bridges.
    name: Scheme Shares
  - description: |
      Schemes are large programmes of work
    name: Schemes
  - description: >
      Site attendances record who was on site and when, whether or not they hold
      an account.

      There is no daily register resource: a day's register and who is currently
      on site are

      both queries over the attendances themselves.
    name: Site Attendances
  - description: |
      Work orders the component parts of a scheme.
    name: Work Orders
  - description: |
      Operations are the work to be carried out within work orders.
    name: Operations
externalDocs:
  description: More documentation and resources
  url: https://docs.ctrl-hub.com
paths:
  /v3/form-submission-stats:
    get:
      tags:
        - Submissions
      description: >
        The aggregate view of an organisation's form submissions: how many there
        are, how

        many fell in a reporting window, how those split by workflow status, and
        the same

        split bucketed over time for a chart.


        Everything windowed is computed from the submissions the same filter
        would list, so

        a figure here and a page of submissions under the same filter describe
        the same

        set. The figures are counted under the caller's own grants, so a
        principal whose

        grants reach part of an organisation is told about that part.


        It exists so a dashboard can ask for counts rather than records.
        Assembling the

        same figures client side means reading every submission in the window
        over the

        network to count it, which is megabytes of payload and a page-by-page
        walk to

        arrive at a few dozen integers.


        The activity series carries every bucket in the reporting window, the
        empty ones

        included, so a consumer renders it as it arrives rather than working out
        for itself

        which buckets held nothing. That is why the window is bounded against
        the bucket

        width: a combination producing more than 1000 buckets is refused with a
        400.


        A sibling collection rather than a sub-resource of a submission, because
        the

        figures belong to the organisation rather than to any one submission.
        The

        organisation is a filter term rather than a path segment, so that
        everything

        selecting the submissions travels in one parameter.


        Served by a hand-written handler rather than a generated one, because
        the resource

        is computed rather than stored.
      operationId: GetFormSubmissionStats
      parameters:
        - $ref: '#/components/parameters/form_submission_stats_filter'
        - $ref: '#/components/parameters/insights_bucket'
      responses:
        '200':
          $ref: '#/components/responses/GetFormSubmissionStats'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorised'
        '500':
          $ref: '#/components/responses/InternalServerError'
      security:
        - Session: []
        - OAuth2: []
        - Cookie: []
components:
  parameters:
    form_submission_stats_filter:
      name: filter
      in: query
      description: >
        Selects the submissions the figures are computed over. Required, because
        it

        carries the organisation and the reporting window.


        - `eq(organisation,<uuid>)`: **Required.** The organisation whose
        submissions the
          figures cover. Exactly one.
        - `ge(created_at,<date-time>)`: **Required.** The start of the reporting
        window.
          Without it the figures would cover every submission the organisation has ever
          taken, which is not a question a dashboard asks.
        - `le(created_at,<date-time>)`: the end of the window. Defaults to now.

        - any other term the submissions list accepts, for example
        `eq(form,<uuid>)` or
          `in(workflow_instance.status,[completed,rejected])`, to narrow the figures.

        Terms are comma separated and combine with AND; an `or(...)` is refused
        with a 400

        rather than answered, because the organisation is read out of the filter
        and the

        rest is applied as a conjunction, so an OR would be answered by a
        predicate other

        than the one asked for. The field names are the form submission
        resource's own, so

        a figure here and a page of submissions under the same filter describe
        the same

        set.


        The window is bounded against the bucket width rather than in its own
        right: the

        series carries every bucket in the window, including the empty ones, so
        a narrow

        width over a wide window is a response far larger than the figures it
        holds. A

        combination producing more than 1000 buckets is refused with a 400
        rather than

        served, so `1d` reaches back about three years and `5m` about three
        days. A consumer

        that widens its bucket as its window widens never approaches the limit.


        The window and the other terms narrow `totals.in_window`, `by_status`
        and

        `activity` only. The four standing counts in `totals` are the
        organisation's

        whole holdings and are deliberately unaffected: they answer how much
        there is,

        which the reporting window does not change.


        For more information on filtering, see [the
        docs](https://docs.ctrl-hub.com/api-reference/features#filtering)
      required: true
      schema:
        type: string
        example: >-
          eq(organisation,7c3d5f11-95a6-4b2e-8f47-19d0c6ba4e83),ge(created_at,2026-08-19T00:00:00Z)
    insights_bucket:
      name: bucket
      in: query
      description: >
        The bucket width for the activity series. The five widths are the ones
        an

        activity chart picks between as the reporting window widens, from five
        minutes

        over a few hours to a week over a year.
      required: false
      schema:
        type: string
        default: 1d
        enum:
          - 5m
          - 1h
          - 6h
          - 1d
          - 1w
  responses:
    GetFormSubmissionStats:
      description: The aggregate submission figures for the organisation.
      headers:
        Content-Type:
          $ref: '#/components/headers/content-type'
        Content-Length:
          $ref: '#/components/headers/content-length'
        X-Request-ID:
          $ref: '#/components/headers/x-request-id'
      content:
        application/vnd.api+json:
          schema:
            allOf:
              - type: object
                required:
                  - data
                properties:
                  data:
                    $ref: '#/components/schemas/FormSubmissionStats'
              - $ref: '#/components/schemas/JSONAPI'
    BadRequest:
      description: >
        There was an error with the request - this could be due to an invalid
        body, query parameters,

        or headers that were sent to the API.
      headers:
        Content-Type:
          $ref: '#/components/headers/content-type'
        Content-Length:
          $ref: '#/components/headers/content-length'
        X-Request-ID:
          $ref: '#/components/headers/x-request-id'
      content:
        application/vnd.api+json:
          schema:
            type: object
            properties:
              errors:
                type: array
                items:
                  $ref: '#/components/schemas/Error'
          example:
            id: 98ca4a78-b66f-4234-9719-aaf832ee6669
            status: '400'
            title: A validation error was encountered
            source:
              parameter: include
            meta:
              resource: wrong_value
    Unauthorised:
      description: Authentication failed
      headers:
        Content-Type:
          $ref: '#/components/headers/content-type'
        Content-Length:
          $ref: '#/components/headers/content-length'
        X-Request-ID:
          $ref: '#/components/headers/x-request-id'
      content:
        application/vnd.api+json:
          schema:
            type: object
            properties:
              errors:
                type: array
                items:
                  $ref: '#/components/schemas/Error'
          example:
            id: 05fc9c8d-73b9-4697-9337-57f7a567a48f
            status: '401'
            title: You are not authorised to access this resource
            detail: In order to access this resource, you need the 'admin' role.
            code: AUTH.001
    InternalServerError:
      description: There was a problem handling the request on the server side
      headers:
        Content-Type:
          $ref: '#/components/headers/content-type'
        Content-Length:
          $ref: '#/components/headers/content-length'
        X-Request-ID:
          $ref: '#/components/headers/x-request-id'
      content:
        application/vnd.api+json:
          schema:
            type: object
            properties:
              errors:
                type: array
                items:
                  $ref: '#/components/schemas/Error'
          example:
            id: fe9d9a69-f0a7-4fdc-bb2c-176027f316c5
            status: '500'
            title: Internal Server Error
            detail: An unexpected error occurred on the server.
  headers:
    content-type:
      description: The content type of the response
      schema:
        type: string
      example: application/vnd.api+json
    content-length:
      description: The length of the response body in bytes
      schema:
        type: integer
        format: int32
      example: 1234
    x-request-id:
      description: >-
        An ID that can be provided when reporting bugs to help identify the
        issue
      schema:
        type: string
      example: 8470f56af4cf25e22be08e72c70dbbdc
  schemas:
    FormSubmissionStats:
      type: object
      description: >
        The aggregate view of an organisation's form submissions. A synthetic,
        read-only

        resource: it is computed from the submissions the caller can see, never
        stored,

        and its identifier is the organisation the figures were computed for.


        Every figure is counted under the caller's own grants, so a principal
        whose grants

        reach part of an organisation is told about that part rather than the
        whole.


        The windowed figures are derived from one grouping of one set of
        submissions, so

        `totals.in_window`, `by_status` and the `activity` series always agree
        with each

        other: each bucket's `total` is its own `by_status` summed, and the
        series sums to

        `by_status`, which sums to `totals.in_window`.
      required:
        - id
        - type
        - attributes
        - relationships
      properties:
        id:
          type: string
          format: uuid
          description: The unique identifier of the organisation the figures cover.
        type:
          type: string
          const: form-submission-stats
        attributes:
          $ref: '#/components/schemas/FormSubmissionStatsAttributes'
        relationships:
          $ref: '#/components/schemas/FormSubmissionStatsRelationships'
    JSONAPI:
      type: object
      description: JSON API response object
      required:
        - jsonapi
      properties:
        jsonapi:
          type: object
          required:
            - version
          properties:
            version:
              type: string
              description: The version of the JSON API specification
              examples:
                - '1.0'
    Error:
      type: object
      description: An error response
      properties:
        id:
          description: >-
            A unique identifier for this particular occurrence of the problem.
            If you encounter this, please provide us with the error ID and we
            can investigate it on our side.
          type: string
          format: uuid
          examples:
            - 05fc9c8d-73b9-4697-9337-57f7a567a48f
        status:
          description: >-
            The status code for the error. This might not match the HTTP status
            code if there are more that one errors to return with different
            status codes.
          type: string
          examples:
            - '401'
            - '500'
        title:
          description: A human readable title for the error.
          type: string
          examples:
            - You are not authorised to access this resource
        detail:
          description: >-
            Where there is more detail that we can provide outside of the title,
            we will provide it here.
          type: string
          examples:
            - In order to access this resource, you need the 'admin' role.
        code:
          description: >-
            A unique code for the error that may help us to diagnose the issue.
            Not all errors have codes, so this is usually empty.
          type: string
          examples:
            - AUTH.001
        source:
          description: A JSON object containing additional information about the error.
          type: object
          properties:
            pointer:
              description: >-
                A JSON Pointer to the value in the request that caused the
                error.
              type: string
              examples:
                - /data/attributes/email
            parameter:
              description: >-
                A string indicating which query parameter in the request caused
                the error.
              type: string
              examples:
                - include
      required:
        - id
        - status
        - title
    FormSubmissionStatsAttributes:
      type: object
      required:
        - totals
        - by_status
        - activity
        - window
      properties:
        totals:
          type: object
          description: >
            The organisation's standing counts, alongside how many submissions
            fell in the

            reporting window.


            The first four ignore the window and every other filter term. They
            answer how

            much there is, which a reporting window does not change, and each is
            counted

            under the caller's grants for that kind of record.
          required:
            - submissions
            - versions
            - forms
            - workflow_definitions
            - in_window
          properties:
            submissions:
              type: integer
              format: int64
              description: Every form submission in the organisation.
            versions:
              type: integer
              format: int64
              description: Every submission version in the organisation.
            forms:
              type: integer
              format: int64
              description: Every form in the organisation.
            workflow_definitions:
              type: integer
              format: int64
              description: Every workflow definition in the organisation.
            in_window:
              type: integer
              format: int64
              description: >
                Submissions created inside the reporting window that match the
                rest of the

                filter. Equal to `by_status` summed, and to the `activity`
                series summed.
        by_status:
          allOf:
            - $ref: '#/components/schemas/FormSubmissionStatusCounts'
            - description: >
                Submissions in the window, partitioned by the type of the
                workflow step

                each one currently sits at. The five counts sum to
                `totals.in_window`, so

                a submission appears in exactly one of them.
        activity:
          type: object
          description: Submission volume over the reporting window, for a chart.
          required:
            - bucket
            - series
          properties:
            bucket:
              type: string
              description: >
                The width each entry in the series covers. Echoes the `bucket`
                parameter,

                or the default when one was not asked for.
              enum:
                - 5m
                - 1h
                - 6h
                - 1d
                - 1w
            series:
              type: array
              description: >
                One entry per bucket in the window, in order, including the
                buckets that

                hold nothing. The empty buckets are carried on purpose: a
                consumer renders

                the series as it arrives rather than seeding its own gaps from a
                window it

                worked out itself, which is how two charts of the same data came
                to

                disagree about where the window started.
              items:
                type: object
                required:
                  - at
                  - total
                  - by_status
                properties:
                  at:
                    type: string
                    format: date-time
                    description: >
                      The start of the bucket, in UTC. Boundaries are whole
                      multiples of

                      the bucket width from the epoch, so they do not depend on
                      anybody's

                      timezone.
                  total:
                    type: integer
                    format: int64
                    description: >-
                      Submissions created in the bucket. Equal to its
                      `by_status` summed.
                  by_status:
                    $ref: '#/components/schemas/FormSubmissionStatusCounts'
        window:
          type: object
          description: >
            The reporting window the figures cover, as it was resolved. A caller
            that gave

            no upper bound can read the instant the window was closed at rather
            than

            assuming it.
          required:
            - from
            - to
          properties:
            from:
              type: string
              format: date-time
            to:
              type: string
              format: date-time
    FormSubmissionStatsRelationships:
      type: object
      description: Relationships for a form submission stats aggregate
      required:
        - organisation
      properties:
        organisation:
          type: object
          required:
            - data
          properties:
            data:
              $ref: '#/components/schemas/OrganisationRelationship'
    FormSubmissionStatusCounts:
      type: object
      description: >
        Submission counts by the type of the workflow step each submission
        currently sits

        at, read from the submission's own snapshot of its workflow position.


        All five are always present, so a caller reading a count never has to
        tell absent

        from zero. A submission with no workflow at all counts as `initial`,
        which is what

        a reader means by "not started".
      required:
        - initial
        - intermediate
        - completed
        - rejected
        - cancelled
      properties:
        initial:
          type: integer
          format: int64
        intermediate:
          type: integer
          format: int64
        completed:
          type: integer
          format: int64
        rejected:
          type: integer
          format: int64
        cancelled:
          type: integer
          format: int64
    OrganisationRelationship:
      type: object
      description: Represents a relationship to an organisation
      required:
        - id
        - type
      properties:
        id:
          type: string
          format: uuid
          description: The unique identifier of the organisation
        type:
          type: string
          const: organisations
  securitySchemes:
    Session:
      description: |
        Session token for authentication.
      in: header
      name: X-Session-Token
      type: apiKey
    OAuth2:
      description: |
        OAuth2 token for authentication.
      flows:
        clientCredentials:
          scopes: {}
          tokenUrl: https://auth.ctrl-hub.com/oauth2/token
      type: oauth2
    Cookie:
      description: |
        Cookie token for authentication.
      in: cookie
      name: ctrl_hub_session
      type: apiKey

````