openapi: 3.1.0
info:
  title: Devin Desktop Analytics API v2
  version: 2.0.0
  description: |
    The Analytics API v2 provides credit and ACU consumption analytics for enterprise teams.
    Data is sourced from hourly-aggregated billing events and supports flexible filtering, grouping,
    and cursor-based pagination.

servers:
  - url: https://server.codeium.com

security:
  - bearerAuth: []

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: |
        A service key with **Analytics Read** permission, passed as a Bearer token in the `Authorization` header.

        Create a service key in your [team settings](https://windsurf.com/team/settings) under the "Service Keys" section.

  schemas:
    Consumption:
      type: object
      description: Usage metrics for the row. Fields are populated based on the team's billing strategy.
      properties:
        prompt_credits:
          type: integer
          format: int64
          description: Prompt credits consumed (credits-based billing only).
        flex_credits:
          type: integer
          format: int64
          description: Flex credits consumed (credits-based billing only).
        billed_acus:
          type: number
          format: double
          description: Billed ACUs consumed (ACU-based billing only).
        message_count:
          type: integer
          format: int64
          description: Number of messages (billing events) in this row. Always populated regardless of billing strategy.

    ConsumptionRow:
      type: object
      required:
        - consumption
      properties:
        timestamp:
          type: string
          description: |
            Time bucket for the row. Format depends on `granularity`: `YYYY-MM-DD` for daily, `YYYY-MM` for monthly.
            Only present when `granularity` is specified.
          examples:
            - "2026-05-01"
            - "2026-05"
        user_id:
          type: string
          description: User identifier (auth UID). Only present when `group_by` includes `user`.
        user_email:
          type: string
          description: User's email address. Only present when `group_by` includes `user`.
          examples:
            - "alice@example.com"
        model_uid:
          type: string
          description: Model identifier. Only present when `group_by` includes `model_uid`.
          examples:
            - "claude-4-sonnet"
        ide:
          type: string
          description: IDE name. Only present when `group_by` includes `ide`.
          examples:
            - "windsurf"
            - "jetbrains"
        ide_version:
          type: string
          description: IDE version. Only present when `group_by` includes `ide_version` (which additionally requires `ide`).
          examples:
            - "1.0.0"
        os:
          type: string
          description: Operating system the request came from. Only present when `group_by` includes `os`.
          examples:
            - "darwin"
            - "windows"
            - "linux"
        consumption:
          $ref: "#/components/schemas/Consumption"

    ConsumptionResponse:
      type: object
      required:
        - data
        - pagination
        - metadata
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/ConsumptionRow"
          description: Array of consumption data rows.
        pagination:
          type: object
          properties:
            next_page_cursor:
              type:
                - string
                - "null"
              description: |
                Opaque cursor for fetching the next page of results. Pass this value as the `page_cursor`
                query parameter in a follow-up request. `null` when there are no more pages.
                Page cursors expire after 24 hours.
        metadata:
          type: object
          properties:
            billing_strategy:
              type: string
              enum:
                - CREDITS
                - ACU
              description: |
                The billing strategy for the authenticated team. Determines which fields in `consumption` are populated:
                - `CREDITS` — `prompt_credits` and `flex_credits`
                - `ACU` — `billed_acus`
            data_freshness:
              type: string
              format: date-time
              description: Timestamp indicating when the underlying data was last refreshed (truncated to the hour).
            query_time_ms:
              type: integer
              format: int64
              description: Server-side query execution time in milliseconds.
            team_id:
              type: string
              description: The team ID resolved from the authenticated service key.
            group_id:
              type: string
              description: The group ID the results were scoped to. Only present when `group_id` was supplied.

    ActiveUsersRow:
      type: object
      required:
        - active_users
      properties:
        timestamp:
          type: string
          description: |
            Time bucket for the row. Format depends on `granularity`: `YYYY-MM-DD` for daily, `YYYY-MM` for monthly.
            Only present when `granularity` is specified.
          examples:
            - "2026-05-01"
            - "2026-05"
        user_id:
          type: string
          description: User identifier (auth UID). Only present when `group_by` includes `user`.
        active_users:
          type: integer
          format: int64
          description: |
            Count of distinct active users in the row. A user is counted as active for a time bucket if
            they have any billing event in it. When grouped by `user`, this is always `1`.

    ActiveUsersResponse:
      type: object
      required:
        - data
        - pagination
        - metadata
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/ActiveUsersRow"
          description: Array of active users data rows.
        pagination:
          type: object
          properties:
            next_page_cursor:
              type:
                - string
                - "null"
              description: |
                Opaque cursor for fetching the next page of results. Pass this value as the `page_cursor`
                query parameter in a follow-up request. `null` when there are no more pages.
                Page cursors expire after 24 hours.
        metadata:
          type: object
          properties:
            data_freshness:
              type: string
              format: date-time
              description: Timestamp indicating when the underlying data was last refreshed (truncated to the hour).
            query_time_ms:
              type: integer
              format: int64
              description: Server-side query execution time in milliseconds.
            team_id:
              type: string
              description: The team ID resolved from the authenticated service key.
            group_id:
              type: string
              description: The group ID the results were scoped to. Only present when `group_id` was supplied.

    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: string
          description: Human-readable error message.

paths:
  /api/v2alpha/analytics/consumption:
    get:
      operationId: getConsumption
      summary: Get consumption analytics
      description: |
        Query credit or ACU consumption data for the authenticated team. Results are sourced from
        hourly-aggregated billing events and can be filtered by date range, product, model, and group.

        The response shape depends on the team's billing strategy:
        - **Credits-based** teams receive `prompt_credits` and `flex_credits` in each row.
        - **ACU-based** teams receive `billed_acus` in each row.

        Responses are cached for 1 hour. Use `If-None-Match` with a previously returned `ETag` to
        receive a `304 Not Modified` when the data has not changed.

        These endpoints are designed for periodic reporting and bulk export. They are **not** intended for real-time usage monitoring: data is hourly-aggregated and the rate limit is low (10 requests per hour per team).
      security:
        - bearerAuth: []
      parameters:
        - name: start_date
          in: query
          required: true
          schema:
            type: string
            format: date
          description: Start of the date range (inclusive) in `YYYY-MM-DD` format.
          example: "2026-01-01"

        - name: end_date
          in: query
          required: true
          schema:
            type: string
            format: date
          description: End of the date range (inclusive) in `YYYY-MM-DD` format. The range must not exceed 90 days.
          example: "2026-01-31"

        - name: product
          in: query
          required: true
          schema:
            type: string
            enum:
              - agent
          description: Product to query consumption for.
          example: "agent"

        - name: granularity
          in: query
          required: false
          schema:
            type: string
            enum:
              - daily
              - monthly
          description: |
            Time granularity for grouping results. When specified, each row includes a `timestamp` field.
            If omitted, results are aggregated across the entire date range.

        - name: group_by
          in: query
          required: false
          schema:
            type: string
          description: |
            Comma-separated list of dimensions to group results by. Supported dimensions:
            - `user` — includes `user_id` and `user_email` in each row
            - `model_uid` — includes `model_uid` in each row
            - `ide` — includes `ide` in each row
            - `ide_version` — includes `ide_version` in each row; requires `ide` to also be included
            - `os` — includes `os` in each row
          example: "user,model_uid"

        - name: models
          in: query
          required: false
          schema:
            type: string
          description: Comma-separated list of model UIDs to filter results to.
          example: "claude-4-sonnet,gpt-4.1"

        - name: group_id
          in: query
          required: false
          schema:
            type: string
          description: Filter results to users in a specific group. The service key must have access to this group.

        - name: user_id
          in: query
          required: false
          schema:
            type: string
          description: Filter results to a specific user (auth UID).

        - name: page_size
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 10000
            default: 1000
          description: Maximum number of rows to return per page.

        - name: page_cursor
          in: query
          required: false
          schema:
            type: string
          description: Opaque cursor from a previous response's `pagination.next_page_cursor` to fetch the next page.

        - name: If-None-Match
          in: header
          required: false
          schema:
            type: string
          description: ETag value from a previous response. If the data has not changed, the server returns `304 Not Modified`.

      responses:
        "200":
          description: Consumption data returned successfully.
          headers:
            ETag:
              schema:
                type: string
              description: Entity tag for cache validation. Pass this as `If-None-Match` in subsequent requests.
            Cache-Control:
              schema:
                type: string
              description: Cache directive (e.g., `private, max-age=3600`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ConsumptionResponse"
              examples:
                credits:
                  summary: Credits-based team with daily granularity
                  value:
                    data:
                      - timestamp: "2026-01-15"
                        user_id: "user_abc123"
                        user_email: "alice@example.com"
                        consumption:
                          prompt_credits: 1250
                          flex_credits: 340
                          message_count: 87
                      - timestamp: "2026-01-15"
                        user_id: "user_def456"
                        user_email: "bob@example.com"
                        consumption:
                          prompt_credits: 980
                          flex_credits: 150
                          message_count: 52
                    pagination:
                      next_page_cursor: null
                    metadata:
                      billing_strategy: "CREDITS"
                      data_freshness: "2026-01-16T03:00:00Z"
                      query_time_ms: 1423
                      team_id: "team_abc123"
                os:
                  summary: Credits-based team grouped by operating system
                  value:
                    data:
                      - os: "darwin"
                        consumption:
                          prompt_credits: 8400
                          flex_credits: 1200
                          message_count: 613
                      - os: "windows"
                        consumption:
                          prompt_credits: 5100
                          flex_credits: 730
                          message_count: 402
                      - os: "linux"
                        consumption:
                          prompt_credits: 1900
                          flex_credits: 260
                          message_count: 155
                    pagination:
                      next_page_cursor: null
                    metadata:
                      billing_strategy: "CREDITS"
                      data_freshness: "2026-01-16T03:00:00Z"
                      query_time_ms: 1105
                      team_id: "team_abc123"
                acu:
                  summary: ACU-based team
                  value:
                    data:
                      - consumption:
                          billed_acus: 42.75
                          message_count: 310
                    pagination:
                      next_page_cursor: null
                    metadata:
                      billing_strategy: "ACU"
                      data_freshness: "2026-01-16T03:00:00Z"
                      query_time_ms: 892
                      team_id: "team_def456"
        "304":
          description: Data has not changed since the ETag provided in `If-None-Match`.
        "400":
          description: Invalid request parameters.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                missing_param:
                  value:
                    error: "start_date is required"
                date_range:
                  value:
                    error: "date range must not exceed 90 days"
                bad_product:
                  value:
                    error: "unsupported product: foo (supported: agent)"
        "401":
          description: Authentication failed or insufficient permissions.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                missing_auth:
                  value:
                    error: "missing Authorization header"
                invalid_key:
                  value:
                    error: "invalid service key"
                insufficient_permissions:
                  value:
                    error: "insufficient permissions"
        "403":
          description: The supplied page cursor does not belong to the authenticated team or requested group.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                cursor_team_mismatch:
                  value:
                    error: "page cursor does not belong to this team"
        "405":
          description: HTTP method not allowed (only `GET` is supported).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          description: Rate limit exceeded (10 requests per hour per team). Paginating an earlier query does not count against this limit.
          headers:
            Retry-After:
              schema:
                type: string
              description: Suggested wait time before retrying.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                rate_limited:
                  value:
                    error: "rate limit exceeded"
        "503":
          description: Analytics service is not available (e.g., in self-hosted deployments).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/v2alpha/analytics/active-users:
    get:
      operationId: getActiveUsers
      summary: Get active users analytics
      description: |
        Query the number of distinct active users for the authenticated team. A user is counted as
        active for a time bucket if they have any billing event in it. Results are sourced from
        hourly-aggregated billing events and can be filtered by date range, product, model, and group.

        Use `granularity` to break the count down per day or month, and `group_by=user` to return one
        row per user (each row's `active_users` will be `1`).

        Responses are cached for 1 hour. Use `If-None-Match` with a previously returned `ETag` to
        receive a `304 Not Modified` when the data has not changed.

        These endpoints are designed for periodic reporting and bulk export. They are **not** intended for real-time usage monitoring: data is hourly-aggregated and the rate limit is low (10 requests per hour per team).
      security:
        - bearerAuth: []
      parameters:
        - name: start_date
          in: query
          required: true
          schema:
            type: string
            format: date
          description: Start of the date range (inclusive) in `YYYY-MM-DD` format.
          example: "2026-01-01"

        - name: end_date
          in: query
          required: true
          schema:
            type: string
            format: date
          description: End of the date range (inclusive) in `YYYY-MM-DD` format. The range must not exceed 90 days.
          example: "2026-01-31"

        - name: product
          in: query
          required: true
          schema:
            type: string
            enum:
              - agent
          description: Product to query active users for.
          example: "agent"

        - name: granularity
          in: query
          required: false
          schema:
            type: string
            enum:
              - daily
              - monthly
          description: |
            Time granularity for grouping results. When specified, each row includes a `timestamp` field.
            If omitted, the active user count is aggregated across the entire date range.

        - name: group_by
          in: query
          required: false
          schema:
            type: string
            enum:
              - user
          description: |
            Dimension to group results by. The active users endpoint only supports `user`, which returns
            one row per active user (each with `active_users` = `1`).
          example: "user"

        - name: models
          in: query
          required: false
          schema:
            type: string
          description: Comma-separated list of model UIDs to filter results to.
          example: "claude-4-sonnet,gpt-4.1"

        - name: group_id
          in: query
          required: false
          schema:
            type: string
          description: Filter results to users in a specific group. The service key must have access to this group.

        - name: user_id
          in: query
          required: false
          schema:
            type: string
          description: Filter results to a specific user (auth UID).

        - name: page_size
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 10000
            default: 1000
          description: Maximum number of rows to return per page.

        - name: page_cursor
          in: query
          required: false
          schema:
            type: string
          description: Opaque cursor from a previous response's `pagination.next_page_cursor` to fetch the next page.

        - name: If-None-Match
          in: header
          required: false
          schema:
            type: string
          description: ETag value from a previous response. If the data has not changed, the server returns `304 Not Modified`.

      responses:
        "200":
          description: Active users data returned successfully.
          headers:
            ETag:
              schema:
                type: string
              description: Entity tag for cache validation. Pass this as `If-None-Match` in subsequent requests.
            Cache-Control:
              schema:
                type: string
              description: Cache directive (e.g., `private, max-age=3600`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ActiveUsersResponse"
              examples:
                aggregate:
                  summary: Total active users over the date range
                  value:
                    data:
                      - active_users: 142
                    pagination:
                      next_page_cursor: null
                    metadata:
                      data_freshness: "2026-01-16T03:00:00Z"
                      query_time_ms: 612
                      team_id: "team_abc123"
                daily:
                  summary: Daily active users
                  value:
                    data:
                      - timestamp: "2026-01-15"
                        active_users: 138
                      - timestamp: "2026-01-16"
                        active_users: 145
                    pagination:
                      next_page_cursor: null
                    metadata:
                      data_freshness: "2026-01-16T03:00:00Z"
                      query_time_ms: 734
                      team_id: "team_abc123"
                by_user:
                  summary: Grouped by user
                  value:
                    data:
                      - user_id: "user_abc123"
                        active_users: 1
                      - user_id: "user_def456"
                        active_users: 1
                    pagination:
                      next_page_cursor: null
                    metadata:
                      data_freshness: "2026-01-16T03:00:00Z"
                      query_time_ms: 845
                      team_id: "team_abc123"
        "304":
          description: Data has not changed since the ETag provided in `If-None-Match`.
        "400":
          description: Invalid request parameters.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                missing_param:
                  value:
                    error: "start_date is required"
                bad_group_by:
                  value:
                    error: "unsupported group_by dimension for active-users: model_uid"
                bad_product:
                  value:
                    error: "unsupported product: foo (supported: agent)"
        "401":
          description: Authentication failed or insufficient permissions.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                missing_auth:
                  value:
                    error: "missing Authorization header"
                invalid_key:
                  value:
                    error: "invalid service key"
                insufficient_permissions:
                  value:
                    error: "insufficient permissions"
        "403":
          description: The supplied page cursor does not belong to the authenticated team or requested group.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                cursor_team_mismatch:
                  value:
                    error: "page cursor does not belong to this team"
        "405":
          description: HTTP method not allowed (only `GET` is supported).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          description: Rate limit exceeded (10 requests per hour per team). Paginating an earlier query does not count against this limit.
          headers:
            Retry-After:
              schema:
                type: string
              description: Suggested wait time before retrying.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                rate_limited:
                  value:
                    error: "rate limit exceeded"
        "503":
          description: Analytics service is not available (e.g., in self-hosted deployments).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
