openapi: 3.0.3

info:
  title: Trakk Agency API
  version: "1"
  description: |
    Programmatic read access to your SEO projects, reports, and AI-generated content.
    Designed for server-to-server integrations.

    ## Authentication
    All requests require a Bearer token in the `Authorization` header:
    ```
    Authorization: Bearer <your-token>
    ```
    Tokens have a status (`active` or `disabled`) and can be restricted to specific IP ranges (single IPs or CIDR ranges).

    ## Rate limits
    Default limits are 60 requests/minute and 1 000 requests/hour. If you exceed a limit the API returns `429` with a `Retry-After` header. Current usage is exposed via `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` response headers.

    ## Response shapes
    - `GET /me` returns a bare object (no `data` wrapper).
    - Single-resource endpoints return `{ "data": { ... } }`.
    - Collection endpoints return `{ "data": [ ... ] }`.
    - Only `GET /projects` is paginated - it also includes `meta` and `links` objects.
    - Cached endpoints include a `cache_refreshed_at` ISO 8601 timestamp. The cache TTL is 1 hour and is invalidated automatically when data changes.

    ## Errors
    All error responses use `{ "error": { "code": "...", "message": "..." } }`.

    ## Versioning
    Non-breaking additions (new fields, new enum values, new parameters, new endpoints) may be added to v1 without a version bump. Build defensively - ignore unknown fields.
  contact:
    email: dev-support@trakk.ai

servers:
  - url: https://admin.trakk.ai/api/v1
    description: Production

security:
  - bearerAuth: []

tags:
  - name: Identity
    description: Verify your token and inspect the associated permissions and agency.
  - name: Projects
    description: List SEO projects and retrieve full project metadata including assigned users, language settings, and contact configuration.
  - name: Reports
    description: Browse monthly reports for a project and check their generation and approval status. Only fetch content when `status` is `completed`.
  - name: Content
    description: |
      Fetch AI-generated SEO content for every keyword in a report.
      Content shape varies by `content_scheme` - some schemes return an array of variants, others a single object.
      See the Content Schemes documentation for exact field names per scheme.
  - name: Keywords
    description: List keywords tracked for a project and see which reports contain content for each keyword.
  - name: Reference Data
    description: Static lookup tables for countries, languages, and Trakk users. Not cached - always returns live data.

# ─────────────────────────────────────────────────────────────────────────────
paths:

  /me:
    get:
      operationId: getMe
      tags: [Identity]
      summary: Verify token and retrieve client identity
      description: |
        Returns the identity associated with your Bearer token. Use this as a quick connectivity and auth check.

        Unlike all other endpoints, the response is a **bare object** - there is no `data` wrapper.
        `agency_id` is `null` for super-admin tokens that have access across all agencies.
        `abilities: ["*"]` indicates full read access to all resources.
      responses:
        "200":
          description: Token is valid. Returns the associated client identity.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Me"
              example:
                id: 3
                name: "Digipartner Integration"
                agency_id: 1
                status: active
                abilities: ["*"]
        "401":
          $ref: "#/components/responses/Unauthenticated"

  /projects:
    get:
      operationId: listProjects
      tags: [Projects]
      summary: List all projects accessible to your token
      description: |
        Returns a paginated list of SEO projects accessible to your token.

        Results are cached for up to 1 hour. The `cache_refreshed_at` field shows when the cache was last populated.

        Use `status` and `search` to narrow results. The `agency_id` filter is available to super-admin tokens only.
        Results are returned 100 per page; use `?page=N` to paginate.
      parameters:
        - name: status
          in: query
          schema:
            $ref: "#/components/schemas/ProjectStatus"
          description: Filter by project lifecycle status.
        - name: search
          in: query
          schema:
            type: string
          description: Partial match against project name or domain.
        - name: agency_id
          in: query
          schema:
            type: integer
          description: Filter by agency ID. Super-admin tokens only.
        - name: page
          in: query
          schema:
            type: integer
            default: 1
          description: Page number (1-based).
        - name: per_page
          in: query
          schema:
            type: integer
            default: 100
            maximum: 100
          description: Number of results per page. Maximum 100.
      responses:
        "200":
          description: Paginated list of projects.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedResponse"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/ProjectListItem"
              example:
                data:
                  - id: 42
                    name: "Digipartner Website"
                    status: active
                    keyword_count: 18
                    latest_report_generated_at: "2026-03-01T10:00:00.000000Z"
                    latest_report_id: 1081
                    latest_report_number: 14
                    latest_report_status: completed
                    links:
                      latest_report: "/api/v1/projects/42/reports/latest"
                meta:
                  current_page: 1
                  per_page: 100
                  total: 3
                  last_page: 1
                  from: 1
                  to: 3
                links:
                  first: "/api/v1/projects?page=1"
                  last: "/api/v1/projects?page=1"
                  prev: null
                  next: null
                cache_refreshed_at: "2026-03-30T12:00:00.000000Z"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"

  /projects/{project}:
    get:
      operationId: getProject
      tags: [Projects]
      summary: Full metadata for a single project
      description: |
        Returns extended project metadata beyond what the list endpoint provides, including assigned users,
        language settings, email configuration, and agency/plan information.

        **Note:** `project.agency` is a plain string (the agency name), unlike the `agency` field inside user
        objects which is `{ id, name }`.
      parameters:
        - $ref: "#/components/parameters/ProjectId"
      responses:
        "200":
          description: Full project metadata.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/ProjectDetail"
                  cache_refreshed_at:
                    $ref: "#/components/schemas/CacheRefreshedAt"
              example:
                data:
                  id: 42
                  name: "Digipartner Website"
                  status: active
                  keyword_count: 18
                  latest_report_generated_at: "2026-03-01T10:00:00.000000Z"
                  latest_report_id: 1081
                  latest_report_number: 14
                  latest_report_status: completed
                  links:
                    latest_report: "/api/v1/projects/42/reports/latest"
                  company_name: "Digipartner AB"
                  domain: "digipartner.se"
                  created_at: "2023-06-15T08:30:00.000000Z"
                  country:
                    id: 1
                    label: "Sweden"
                  text_language:
                    id: 3
                    label: "Swedish"
                  report_language:
                    id: 4
                    label: "English"
                  managing_user:
                    id: 7
                    name: "Jane Smith"
                    role: "Account Manager"
                    agency: {id: 12, name: "Trakk"}
                  created_by:
                    id: 5
                    name: "John Doe"
                    role: "Admin"
                    agency: {id: 12, name: "Trakk"}
                  contact_email: "contact@digipartner.se"
                  client_emails: ["ceo@digipartner.se", "marketing@digipartner.se"]
                  cc_email: null
                  receives_monthly_reports: true
                  agency: "Trakk"
                  plan: "Growth"
                  gsc_status: "connected"
                cache_refreshed_at: "2026-03-30T12:00:00.000000Z"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /projects/{project}/reports:
    get:
      operationId: listReports
      tags: [Reports]
      summary: All reports for a project, newest first
      description: |
        Returns all reports for a project sorted newest first. A project can have up to 26 reports,
        one for each position in the content delivery sequence.

        Only fetch content (via the content endpoint) for reports where `status === "completed"`.
        Use `report_number` as a stable sync key - it identifies the report's position in the sequence
        and never changes, unlike the database `id`.
      parameters:
        - $ref: "#/components/parameters/ProjectId"
      responses:
        "200":
          description: List of reports for the project, newest first.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Report"
                  cache_refreshed_at:
                    $ref: "#/components/schemas/CacheRefreshedAt"
              example:
                data:
                  - id: 1081
                    project_id: 42
                    report_number: 14
                    report_name: "H3 + Text 3"
                    status: completed
                    date_to_create_texts: "2026-02-20"
                    date_to_send_to_client: "2026-03-01"
                    sent: true
                    approved_status: approved
                    approved_by:
                      id: 7
                      name: "Jane Smith"
                      role: "Account Manager"
                      agency: {id: 12, name: "Trakk"}
                    managed_status: completed
                    managed_by:
                      id: 7
                      name: "Jane Smith"
                      role: "Account Manager"
                      agency: {id: 12, name: "Trakk"}
                    pdf_link: "https://admin.trakk.ai/reports/digipartner-report-14.pdf"
                    web_link: "https://admin.trakk.ai/reports/view/abc123"
                  - id: 1044
                    project_id: 42
                    report_number: 13
                    report_name: "H3 + Text 2"
                    status: completed
                    date_to_create_texts: "2026-01-20"
                    date_to_send_to_client: "2026-02-01"
                    sent: true
                    approved_status: approved
                    approved_by:
                      id: 7
                      name: "Jane Smith"
                      role: "Account Manager"
                      agency: {id: 12, name: "Trakk"}
                    managed_status: completed
                    managed_by:
                      id: 7
                      name: "Jane Smith"
                      role: "Account Manager"
                      agency: {id: 12, name: "Trakk"}
                    pdf_link: "https://admin.trakk.ai/reports/digipartner-report-13.pdf"
                    web_link: "https://admin.trakk.ai/reports/view/def456"
                cache_refreshed_at: "2026-03-30T12:00:00.000000Z"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /projects/{project}/reports/latest:
    get:
      operationId: getLatestReport
      tags: [Reports]
      summary: The most recently created report
      description: |
        Returns the most recently created report for a project. Returns `404` if the project has no reports yet.

        This is the recommended polling endpoint to detect new reports: compare the returned `report_number`
        with the last value you stored. If it's higher, a new report is available and you can fetch its content.

        Always check `status === "completed"` before calling the content endpoint.
      parameters:
        - $ref: "#/components/parameters/ProjectId"
      responses:
        "200":
          description: Most recent report for the project.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Report"
                  cache_refreshed_at:
                    $ref: "#/components/schemas/CacheRefreshedAt"
              example:
                data:
                  id: 1081
                  project_id: 42
                  report_number: 14
                  report_name: "H3 + Text 3"
                  status: completed
                  date_to_create_texts: "2026-02-20"
                  date_to_send_to_client: "2026-03-01"
                  sent: true
                  approved_status: approved
                  approved_by:
                    id: 7
                    name: "Jane Smith"
                    role: "Account Manager"
                    agency: {id: 12, name: "Trakk"}
                  managed_status: completed
                  managed_by:
                    id: 7
                    name: "Jane Smith"
                    role: "Account Manager"
                    agency: {id: 12, name: "Trakk"}
                  pdf_link: "https://admin.trakk.ai/reports/digipartner-report-14.pdf"
                  web_link: "https://admin.trakk.ai/reports/view/abc123"
                cache_refreshed_at: "2026-03-30T12:00:00.000000Z"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /projects/{project}/reports/{report_number}/content:
    get:
      operationId: getReportContent
      tags: [Content]
      summary: Full generated SEO content for a report, grouped by keyword
      description: |
        The primary integration endpoint. Returns AI-generated SEO content for every keyword tracked in a project,
        for the specified report position.

        **`report_number`** is the report's position in the delivery sequence (1-26), **not** its database `id`.
        Use the `report_number` from the reports endpoints to construct this path.

        **Content shape** varies by `content_scheme`:
        - Array schemes (`metadata`, `h1_headlines`, `h2_text_blocks`, `h3_text_blocks`, `h4_text_blocks`, `qa_block`, `link_blocks`) -
          `content` and `html` are arrays of variant objects; pick the variant that fits best.
        - Object schemes (`single_text_block`, `h2_summary_block`, `subnavigation`, `bullet_points`, `numbered_points`, `image_alt_text`) -
          `content` and `html` are single objects.

        `content_scheme_fields` lists the field names present in each content block for this report.
        Keywords without generated content have `has_content: false` and `content: null`.

        Returns `400` if `report_number` is outside the valid range (1-26).
        Returns `404` if no report exists at that position for the project.
      parameters:
        - $ref: "#/components/parameters/ProjectId"
        - name: report_number
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
            maximum: 26
          description: Report's position in the project's delivery sequence (1-26). Not the report database ID.
      responses:
        "200":
          description: Generated SEO content for all keywords in the report.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/ReportContent"
                  cache_refreshed_at:
                    $ref: "#/components/schemas/CacheRefreshedAt"
              example:
                data:
                  report_number: 14
                  report_id: 1081
                  report_name: "H3 + Text 3"
                  content_scheme: h3_text_blocks
                  content_scheme_fields: ["h3", "text"]
                  links:
                    project_website: "https://digipartner.se"
                    report_web: "https://admin.trakk.ai/reports/view/abc123"
                    report_pdf: "https://admin.trakk.ai/reports/digipartner-report-14.pdf"
                  project_id: 42
                  project_domain: "digipartner.se"
                  project_status: active
                  is_generated: true
                  generation_status: completed
                  approved_status: approved
                  approved_by:
                    id: 7
                    name: "Jane Smith"
                    role: "Account Manager"
                    agency: {id: 12, name: "Trakk"}
                  managed_status: completed
                  managed_by:
                    id: 7
                    name: "Jane Smith"
                    role: "Account Manager"
                    agency: {id: 12, name: "Trakk"}
                  date_to_create_texts: "2026-02-20"
                  date_to_send_to_client: "2026-03-01"
                  summary:
                    keyword_count: 18
                    keywords_with_content: 16
                  keywords:
                    - keyword_id: 901
                      keyword: "seo agency stockholm"
                      keyword_url: "https://digipartner.se/seo-stockholm"
                      has_content: true
                      content:
                        - h3: "Experienced SEO Agency in Stockholm"
                          text: "We help Stockholm businesses rank higher with proven SEO strategies."
                        - h3: "Stockholm's Trusted SEO Partner"
                          text: "Data-driven SEO tailored to the Stockholm market."
                        - h3: "Grow Your Business with SEO in Stockholm"
                          text: "Our Stockholm SEO team delivers measurable organic growth."
                      html:
                        - h3: "<h3>Experienced SEO Agency in Stockholm</h3>"
                          text: "<p>We help Stockholm businesses rank higher with proven SEO strategies.</p>"
                        - h3: "<h3>Stockholm's Trusted SEO Partner</h3>"
                          text: "<p>Data-driven SEO tailored to the Stockholm market.</p>"
                        - h3: "<h3>Grow Your Business with SEO in Stockholm</h3>"
                          text: "<p>Our Stockholm SEO team delivers measurable organic growth.</p>"
                    - keyword_id: 902
                      keyword: "seo consultant sweden"
                      keyword_url: null
                      has_content: false
                      content: null
                      html: null
                cache_refreshed_at: "2026-03-30T12:00:00.000000Z"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /projects/{project}/keywords:
    get:
      operationId: listKeywords
      tags: [Keywords]
      summary: Keywords for a project with content coverage
      description: |
        Returns all keywords tracked for a project, with content coverage statistics.

        `content_action_ids` is a sorted array of report numbers (1-26) that contain generated content for
        the keyword. This lets you quickly audit which reports have covered a keyword without fetching content.

        Use `content_count === 0` to find keywords that have never received content, or compare
        `content_action_ids` against your expected set to spot gaps.
      parameters:
        - $ref: "#/components/parameters/ProjectId"
      responses:
        "200":
          description: List of keywords with content coverage statistics.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Keyword"
                  cache_refreshed_at:
                    $ref: "#/components/schemas/CacheRefreshedAt"
              example:
                data:
                  - id: 901
                    keyword: "seo agency stockholm"
                    url: "https://digipartner.se/seo-stockholm"
                    create_date: "2024-01-15"
                    latest_position: 7
                    status_200: true
                    content_count: 5
                    content_action_ids: [1, 4, 8, 11, 14]
                  - id: 902
                    keyword: "seo consultant sweden"
                    url: null
                    create_date: "2024-01-15"
                    latest_position: null
                    status_200: null
                    content_count: 0
                    content_action_ids: []
                cache_refreshed_at: "2026-03-30T12:00:00.000000Z"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /countries:
    get:
      operationId: listCountries
      tags: [Reference Data]
      summary: List all countries
      description: Returns the full list of countries available in Trakk. Use IDs to resolve the `country` field on project detail objects. Not cached.
      responses:
        "200":
          description: List of all countries.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/LabelledItem"
              example:
                data:
                  - id: 1
                    label: "Sweden"
                  - id: 2
                    label: "Norway"
        "401":
          $ref: "#/components/responses/Unauthenticated"

  /countries/{country}:
    get:
      operationId: getCountry
      tags: [Reference Data]
      summary: Single country by ID
      description: Returns a single country by its ID. Not cached.
      parameters:
        - name: country
          in: path
          required: true
          schema:
            type: integer
          description: Country ID.
      responses:
        "200":
          description: Country.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/LabelledItem"
              example:
                data:
                  id: 1
                  label: "Sweden"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "404":
          $ref: "#/components/responses/NotFound"

  /languages:
    get:
      operationId: listLanguages
      tags: [Reference Data]
      summary: List all languages
      description: Returns all languages available in Trakk. Used to resolve `text_language` and `report_language` on project detail objects. Not cached.
      responses:
        "200":
          description: List of all languages.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/LabelledItem"
              example:
                data:
                  - id: 3
                    label: "Swedish"
                  - id: 4
                    label: "English"
        "401":
          $ref: "#/components/responses/Unauthenticated"

  /languages/{language}:
    get:
      operationId: getLanguage
      tags: [Reference Data]
      summary: Single language by ID
      description: Returns a single language by its ID. Not cached.
      parameters:
        - name: language
          in: path
          required: true
          schema:
            type: integer
          description: Language ID.
      responses:
        "200":
          description: Language.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/LabelledItem"
              example:
                data:
                  id: 3
                  label: "Swedish"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "404":
          $ref: "#/components/responses/NotFound"

  /users:
    get:
      operationId: listUsers
      tags: [Reference Data]
      summary: List users visible to your token
      description: |
        Returns all Trakk users visible to your token. Use to resolve `managing_user`, `created_by`,
        `approved_by`, and `managed_by` fields on project and report objects. Not cached.
      responses:
        "200":
          description: List of users.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/User"
              example:
                data:
                  - id: 7
                    name: "Jane Smith"
                    role: "Account Manager"
                    agency: {id: 12, name: "Trakk"}
                  - id: 5
                    name: "John Doe"
                    role: "Admin"
                    agency: {id: 12, name: "Trakk"}
        "401":
          $ref: "#/components/responses/Unauthenticated"

  /users/{user}:
    get:
      operationId: getUser
      tags: [Reference Data]
      summary: Single user by ID
      description: Returns a single Trakk user by their ID. Not cached.
      parameters:
        - name: user
          in: path
          required: true
          schema:
            type: integer
          description: User ID.
      responses:
        "200":
          description: User.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/User"
              example:
                data:
                  id: 7
                  name: "Jane Smith"
                  role: "Account Manager"
                  agency: {id: 12, name: "Trakk"}
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "404":
          $ref: "#/components/responses/NotFound"

# ─────────────────────────────────────────────────────────────────────────────
components:

  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: "Pass your API token as `Authorization: Bearer <token>` on every request."

  parameters:
    ProjectId:
      name: project
      in: path
      required: true
      schema:
        type: integer
      description: Project ID.

  # ── Reusable responses ──────────────────────────────────────────────────────
  responses:
    Unauthenticated:
      description: Missing or invalid Bearer token.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
          example:
            error:
              code: unauthenticated
              message: "Missing or invalid Bearer token."

    Forbidden:
      description: Token is valid but access is denied (wrong agency or non-whitelisted IP).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
          examples:
            wrongAgency:
              summary: Project belongs to a different agency
              value:
                error:
                  code: forbidden
                  message: "You do not have permission to access this resource."
            ipNotAllowed:
              summary: Request from non-whitelisted IP
              value:
                error:
                  code: forbidden
                  message: "Your IP address is not allowed to use this API key."

    NotFound:
      description: Resource not found.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
          example:
            error:
              code: not_found
              message: "The requested resource was not found."

    BadRequest:
      description: Bad request - unknown parameter, out-of-range value, or invalid input.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
          examples:
            unknownParam:
              summary: Unknown query parameter
              value:
                error:
                  code: bad_request
                  message: "Unknown query parameter(s): statuss. Allowed: status, search, agency_id, page, per_page."
            outOfRange:
              summary: report_number out of range
              value:
                error:
                  code: bad_request
                  message: "report_number must be between 1 and 26."

  # ── Schemas ─────────────────────────────────────────────────────────────────
  schemas:

    ErrorEnvelope:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              enum:
                - unauthenticated
                - forbidden
                - not_found
                - bad_request
                - method_not_allowed
                - validation_error
                - rate_limit_exceeded
                - server_error
            message:
              type: string
              description: Human-readable explanation of the error.

    CacheRefreshedAt:
      type: string
      format: date-time
      description: ISO 8601 timestamp of when this response was last cached. Cache TTL is 1 hour; invalidated automatically on data changes.
      example: "2026-03-30T12:00:00.000000Z"

    PaginationMeta:
      type: object
      description: Metadata about the current page of results.
      properties:
        current_page:
          type: integer
          description: The current page number (1-based).
        per_page:
          type: integer
          description: Maximum number of items per page.
        total:
          type: integer
          description: Total number of items across all pages.
        last_page:
          type: integer
          description: Number of the last page.
        from:
          type: integer
          nullable: true
          description: 1-based index of the first item on this page. Null if the page is empty.
        to:
          type: integer
          nullable: true
          description: 1-based index of the last item on this page. Null if the page is empty.

    PaginationLinks:
      type: object
      description: Relative URL links for navigating between pages.
      properties:
        first:
          type: string
          nullable: true
          description: URL of the first page.
        last:
          type: string
          nullable: true
          description: URL of the last page.
        prev:
          type: string
          nullable: true
          description: URL of the previous page. Null on the first page.
        next:
          type: string
          nullable: true
          description: URL of the next page. Null on the last page.

    PaginatedResponse:
      type: object
      properties:
        data:
          type: array
          items: {}
        meta:
          $ref: "#/components/schemas/PaginationMeta"
        links:
          $ref: "#/components/schemas/PaginationLinks"
        cache_refreshed_at:
          $ref: "#/components/schemas/CacheRefreshedAt"

    LabelledItem:
      type: object
      description: A simple ID + label pair used for countries and languages.
      properties:
        id:
          type: integer
          description: Unique identifier.
        label:
          type: string
          description: Human-readable name (e.g. "Sweden", "Swedish").

    User:
      type: object
      description: A Trakk platform user embedded in project and report objects.
      properties:
        id:
          type: integer
          description: Unique user identifier.
        name:
          type: string
          description: Full name of the user.
        role:
          type: string
          nullable: true
          description: User's role within their agency (e.g. "Account Manager"). Null if not set.
        agency:
          type: object
          nullable: true
          description: Agency the user belongs to. Null for super-admin users.
          properties:
            id:
              type: integer
              description: Agency ID.
            name:
              type: string
              description: Agency name.

    Me:
      type: object
      description: Identity associated with the Bearer token. Returned as a bare object (no `data` wrapper).
      properties:
        id:
          type: integer
          description: Unique identifier of the API token's associated client record.
        name:
          type: string
          description: Display name of the token, often reflecting the agency or integration name.
        agency_id:
          type: integer
          nullable: true
          description: Agency this token belongs to. Null for super-admin tokens that have cross-agency access.
        status:
          type: string
          enum: [active, disabled]
          description: Whether the token is currently active and usable.
        abilities:
          type: array
          items:
            type: string
          description: Permission scopes granted to this token. `["*"]` means full read access to all resources.

    ProjectStatus:
      type: string
      description: Lifecycle status of a project.
      enum:
        - active
        - pending
        - "pending - in progress"
        - paused
        - ended
        - "termination pending"

    ReportStatus:
      type: string
      description: Generation status of a report. Only fetch content when this is `completed`.
      enum: [pending, running, completed, failed]

    ContentScheme:
      type: string
      description: |
        Determines the structure of `content` and `html` in each keyword object of a report.

        **Array schemes** - `content` and `html` are arrays of variant objects (pick the one that fits best):
        - `metadata` - 3 variants, each with `meta_title` and `meta_description`
        - `h1_headlines` - 3 variants, each with `h1`
        - `h2_text_blocks` - 3 variants, each with `h2` and `text`
        - `h3_text_blocks` - 3 variants, each with `h3` and `text`
        - `h4_text_blocks` - 6 variants, each with `h4` and `text`
        - `qa_block` - 4 Q&A pairs in `content`, single rendered block in `html`
        - `link_blocks` - 2 variants with heading, text, and internal links

        **Object schemes** - `content` and `html` are single objects:
        - `single_text_block` - single `text` field
        - `h2_summary_block` - `h2` + `texts` array of 2 excerpts
        - `subnavigation` - `links` array of `{ keyword, url }` pairs
        - `bullet_points` - `h2`, `text`, and `bullet_points` array
        - `numbered_points` - `h2`, `text`, and `numbered_points` array
        - `image_alt_text` - `search_terms` (2 items) and `alt_texts` (4 items)
      enum:
        - single_text_block
        - metadata
        - h1_headlines
        - h2_text_blocks
        - h3_text_blocks
        - h4_text_blocks
        - qa_block
        - image_alt_text
        - link_blocks
        - h2_summary_block
        - subnavigation
        - bullet_points
        - numbered_points

    ProjectListItem:
      type: object
      description: Summary of a project as returned in the paginated list.
      properties:
        id:
          type: integer
          description: Unique project identifier.
        name:
          type: string
          description: Display name of the project.
        status:
          $ref: "#/components/schemas/ProjectStatus"
        keyword_count:
          type: integer
          description: Number of keywords being tracked for this project.
        latest_report_generated_at:
          type: string
          format: date-time
          nullable: true
          description: ISO 8601 datetime when the most recent report was generated. Null if no reports exist.
        latest_report_id:
          type: integer
          nullable: true
          description: Database ID of the most recent report. Null if no reports exist.
        latest_report_number:
          type: integer
          nullable: true
          description: Delivery sequence position (1-26) of the most recent report. Use to detect when a new report has been created.
        latest_report_status:
          type: string
          nullable: true
          enum: [pending, running, completed, failed]
          description: Generation status of the most recent report. Only fetch content when this is `completed`.
        links:
          type: object
          properties:
            latest_report:
              type: string
              description: Relative API path to the latest report endpoint for this project.

    ProjectDetail:
      allOf:
        - $ref: "#/components/schemas/ProjectListItem"
        - type: object
          description: Extended project metadata including users, language settings, and contact configuration.
          properties:
            company_name:
              type: string
              nullable: true
              description: Legal company name associated with this project.
            domain:
              type: string
              nullable: true
              description: Primary domain of the project website (e.g. "example.com").
            created_at:
              type: string
              format: date-time
              description: ISO 8601 datetime when the project was created.
            country:
              type: object
              nullable: true
              description: Country the project is targeting. Null if not set.
              properties:
                id:
                  type: integer
                label:
                  type: string
            text_language:
              type: object
              nullable: true
              description: Language used for generating content text. Null if not set.
              properties:
                id:
                  type: integer
                label:
                  type: string
            report_language:
              type: object
              nullable: true
              description: Language used for the report document. Null if not set.
              properties:
                id:
                  type: integer
                label:
                  type: string
            managing_user:
              type: object
              nullable: true
              description: Trakk user responsible for managing this project. Null if unassigned.
              allOf:
                - $ref: "#/components/schemas/User"
            created_by:
              type: object
              nullable: true
              description: Trakk user who created the project. Null if unknown.
              allOf:
                - $ref: "#/components/schemas/User"
            contact_email:
              type: string
              nullable: true
              description: Primary contact email for the project. Null if not set.
            client_emails:
              type: array
              items:
                type: string
              description: Email addresses of client contacts who receive reports.
            cc_email:
              type: string
              nullable: true
              description: CC email address for report delivery. Null if not set.
            receives_monthly_reports:
              type: boolean
              nullable: true
              description: Whether the client receives monthly report emails.
            agency:
              type: string
              nullable: true
              description: |
                Name of the agency managing this project, as a plain string.
                Note: this differs from the `agency` field inside user objects, which is `{ id, name }`.
            plan:
              type: string
              nullable: true
              description: Subscription plan name for this project (e.g. "Growth").
            gsc_status:
              type: string
              nullable: true
              description: Google Search Console connection status for this project.

    Report:
      type: object
      description: A monthly SEO report for a project.
      properties:
        id:
          type: integer
          description: Unique report identifier (database ID).
        project_id:
          type: integer
          description: ID of the project this report belongs to.
        report_number:
          type: integer
          description: Position in the project's delivery sequence (1-26). Use as a stable sync key - this never changes.
        report_name:
          type: string
          nullable: true
          description: Human-readable name reflecting the content scheme used (e.g. "H3 + Text 3").
        status:
          $ref: "#/components/schemas/ReportStatus"
        date_to_create_texts:
          type: string
          format: date
          nullable: true
          description: Scheduled date for content generation (YYYY-MM-DD). Null if not scheduled.
        date_to_send_to_client:
          type: string
          format: date
          nullable: true
          description: Scheduled date for delivering the report to the client (YYYY-MM-DD). Null if not scheduled.
        sent:
          type: boolean
          description: Whether the report has been sent to the client.
        approved_status:
          type: string
          nullable: true
          enum: [waiting, on-hold, approved, blocked]
          description: Editorial approval state. Null if the approval workflow is not in use.
        approved_by:
          type: object
          nullable: true
          description: User who approved the report. Null if not yet approved.
          allOf:
            - $ref: "#/components/schemas/User"
        managed_status:
          type: string
          nullable: true
          enum: [in-progress, on-hold, completed, waiting-on-client]
          description: Internal project management state. Null if not set.
        managed_by:
          type: object
          nullable: true
          description: User responsible for managing this report. Null if unassigned.
          allOf:
            - $ref: "#/components/schemas/User"
        pdf_link:
          type: string
          nullable: true
          description: Direct URL to the PDF version of the report. Null until the PDF has been generated.
        web_link:
          type: string
          nullable: true
          description: Direct URL to the web version of the report. Null until the web view has been generated.

    ReportContent:
      type: object
      description: Full AI-generated SEO content for all keywords in a report.
      properties:
        report_number:
          type: integer
          description: Delivery sequence position (1-26).
        report_id:
          type: integer
          description: Database ID of this report.
        report_name:
          type: string
          nullable: true
          description: Human-readable name reflecting the content scheme (e.g. "H3 + Text 3").
        content_scheme:
          type: string
          nullable: true
          description: Determines the structure of `content` and `html` in each keyword object. See ContentScheme for details.
          allOf:
            - $ref: "#/components/schemas/ContentScheme"
        content_scheme_fields:
          type: array
          nullable: true
          items:
            type: string
          description: Field names present in each content block for this scheme (e.g. `["h3", "text"]`). Use to know which keys to read from each content/html object.
        links:
          type: object
          description: Useful URLs related to this report.
          properties:
            project_website:
              type: string
              nullable: true
              description: The project's target website URL.
            report_web:
              type: string
              nullable: true
              description: URL to the web version of this report.
            report_pdf:
              type: string
              nullable: true
              description: URL to the PDF version of this report.
        project_id:
          type: integer
          description: ID of the project this content belongs to.
        project_domain:
          type: string
          nullable: true
          description: Primary domain of the project (e.g. "example.com").
        project_status:
          $ref: "#/components/schemas/ProjectStatus"
        is_generated:
          type: boolean
          description: Convenience flag - true when `generation_status` is `completed`.
        generation_status:
          type: string
          enum: [pending, running, completed, failed]
          description: Content generation status. Same values as report `status`, exposed under this name on the content endpoint.
        approved_status:
          type: string
          nullable: true
          enum: [waiting, on-hold, approved, blocked]
          description: Editorial approval state for this content.
        approved_by:
          type: object
          nullable: true
          description: User who approved this content. Null if not yet approved.
          allOf:
            - $ref: "#/components/schemas/User"
        managed_status:
          type: string
          nullable: true
          enum: [in-progress, on-hold, completed, waiting-on-client]
          description: Internal management state for this content.
        managed_by:
          type: object
          nullable: true
          description: User responsible for managing this content. Null if unassigned.
          allOf:
            - $ref: "#/components/schemas/User"
        date_to_create_texts:
          type: string
          format: date
          nullable: true
          description: Scheduled content creation date (YYYY-MM-DD).
        date_to_send_to_client:
          type: string
          format: date
          nullable: true
          description: Scheduled client delivery date (YYYY-MM-DD).
        summary:
          type: object
          description: Aggregate content coverage for this report.
          properties:
            keyword_count:
              type: integer
              description: Total number of keywords in this report.
            keywords_with_content:
              type: integer
              description: Number of keywords that have generated content.
        keywords:
          type: array
          description: "One entry per keyword tracked in this project. Keywords without content have `has_content: false` and `content: null`."
          items:
            $ref: "#/components/schemas/ReportKeyword"

    ReportKeyword:
      type: object
      description: A keyword and its generated content within a report.
      properties:
        keyword_id:
          type: integer
          description: Unique identifier of the keyword.
        keyword:
          type: string
          description: The keyword string (e.g. "seo agency stockholm").
        keyword_url:
          type: string
          nullable: true
          description: Target URL this keyword is mapped to. Null if no URL has been assigned.
        has_content:
          type: boolean
          description: Whether generated content exists for this keyword in this report. When false, `content` and `html` are null.
        content:
          type: object
          nullable: true
          description: |
            Plain-text content. Null when `has_content` is false.
            Shape depends on `content_scheme`:
            - Array schemes: an array of variant objects (e.g. `[{ "h3": "...", "text": "..." }, ...]`)
            - Object schemes: a single object (e.g. `{ "h2": "...", "texts": ["...", "..."] }`)
            Use `content_scheme_fields` at the report level to know which keys to read.
        html:
          type: object
          nullable: true
          description: |
            HTML-formatted version of the content. Null when `has_content` is false.
            Mirrors the shape of `content` - use this to embed content directly into a CMS without additional formatting.

    Keyword:
      type: object
      description: A keyword tracked for a project, with content coverage statistics.
      properties:
        id:
          type: integer
          description: Unique keyword identifier.
        keyword:
          type: string
          description: The keyword string (e.g. "seo agency stockholm").
        url:
          type: string
          nullable: true
          description: Target URL this keyword is mapped to. Null if no URL has been assigned.
        create_date:
          type: string
          format: date
          description: Date the keyword was added to the project (YYYY-MM-DD).
        latest_position:
          type: integer
          nullable: true
          description: Most recent SERP rank position (1-based). Null if position has not been tracked yet.
        status_200:
          type: boolean
          nullable: true
          description: Whether the target URL returned HTTP 200 on the last check. Null if the URL has not been checked yet.
        content_count:
          type: integer
          description: Number of reports that contain generated content for this keyword.
        content_action_ids:
          type: array
          items:
            type: integer
          description: Sorted array of report numbers (1-26) that have content for this keyword. Empty array when `content_count` is 0.
