openapi: 3.1.0
info:
  title: LinkShift Management API (API Key Accessible Endpoints)
  version: 1.0.0
  summary: Organization-scoped redirect management API
  description: |
    This specification describes the LinkShift endpoints that can be accessed with organization API keys.

    Authentication:
    - Preferred: `X-API-Key: <your_key>`
    - Alternative: `Authorization: ApiKey <your_key>` (supported by backend, not modeled as a separate OpenAPI scheme)

    Important billing behavior:
    - API keys are organization-scoped.
    - API key management endpoints (`/api/v1/api-keys`) are intentionally excluded and require dashboard user auth.

    Rate limiting:
    - Enforced per API key (not per organization).
    - Basic: 10 requests/minute/key.
    - Pro: 50 requests/minute/key.
servers:
  - url: https://linkshift.app
    description: Production API host
security:
  - ApiKeyAuth: []
tags:
  - name: Organization
    description: Organization-scoped operational data available for the current API key.
  - name: Domain Groups
    description: Domain group management, including robots policy defaults for assigned domains.
  - name: Domains
    description: Domain inventory and grouping used by redirect execution.
  - name: Redirect Rules
    description: Core redirect routing rules, analytics, and simulation endpoints.
  - name: Redirect Tests
    description: Stored redirect test fixtures for regression and CI validation.
  - name: Link Maps
    description: Key-value routing maps referenced by redirect rules.
  - name: Link Map Entries
    description: Entry-level CRUD and bulk import operations for link maps.
paths:
  /api/v1/organization/usage:
    get:
      tags: [Organization]
      summary: Get organization usage summary
      description: Returns current resource usage for the authenticated organization.
      operationId: getOrganizationUsage
      responses:
        '200':
          description: Usage summary
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrganizationUsage'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /api/v1/domain-groups:
    get:
      tags: [Domain Groups]
      summary: List domain groups
      description: Returns all active domain groups owned by the authenticated organization.
      operationId: listDomainGroups
      responses:
        '200':
          description: Domain groups list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DomainGroupQueryResult'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    post:
      tags: [Domain Groups]
      summary: Create domain group
      description: Creates a domain group and optionally configures robots policy defaults.
      operationId: createDomainGroup
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateDomainGroupRequest'
      responses:
        '200':
          description: Created domain group
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DomainGroup'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /api/v1/domain-groups/{id}:
    parameters:
      - $ref: '#/components/parameters/EntityId'
    get:
      tags: [Domain Groups]
      summary: Get domain group
      description: Returns one domain group by ID when it belongs to the authenticated organization.
      operationId: getDomainGroup
      responses:
        '200':
          description: Domain group details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DomainGroup'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    put:
      tags: [Domain Groups]
      summary: Update domain group
      description: Updates mutable domain-group fields such as name and robots policy options.
      operationId: updateDomainGroup
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateDomainGroupRequest'
      responses:
        '200':
          description: Updated domain group
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DomainGroup'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    delete:
      tags: [Domain Groups]
      summary: Delete domain group
      description: Marks a domain group as deleted without physically removing historical records.
      operationId: deleteDomainGroup
      responses:
        '200':
          description: Deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /api/v1/domains:
    get:
      tags: [Domains]
      summary: List domains
      description: Returns all active domains for the authenticated organization.
      operationId: listDomains
      responses:
        '200':
          description: Domains list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DomainQueryResult'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    post:
      tags: [Domains]
      summary: Create domain
      description: Creates a new domain in a target domain group after uniqueness and ownership checks.
      operationId: createDomain
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateDomainRequest'
      responses:
        '200':
          description: Created domain
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Domain'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /api/v1/domains/{id}:
    parameters:
      - $ref: '#/components/parameters/EntityId'
    get:
      tags: [Domains]
      summary: Get domain
      description: Returns one domain by ID when it belongs to the authenticated organization.
      operationId: getDomain
      responses:
        '200':
          description: Domain details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Domain'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    put:
      tags: [Domains]
      summary: Update domain
      description: Updates mutable domain fields such as name or assigned domain group.
      operationId: updateDomain
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateDomainRequest'
      responses:
        '200':
          description: Updated domain
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Domain'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    delete:
      tags: [Domains]
      summary: Delete domain
      description: Marks a domain as deleted without hard-removing it from storage.
      operationId: deleteDomain
      responses:
        '200':
          description: Deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /api/v1/redirect-rules:
    get:
      tags: [Redirect Rules]
      summary: List redirect rules
      description: |
        Cursor-style pagination ordered by `priority desc, createdAt desc, id desc`.
      operationId: listRedirectRules
      parameters:
        - name: domainGroupId
          in: query
          required: true
          description: Domain group scope for listing rules.
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Maximum number of items to return (cursor page size).
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: search
          in: query
          required: false
          description: Free-text search over rule source/destination fields.
          schema:
            type: string
        - name: startAfterId
          in: query
          required: false
          description: Cursor token using the last item ID from previous page.
          schema:
            type: string
      responses:
        '200':
          description: Redirect rules list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RedirectRuleQueryResult'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    post:
      tags: [Redirect Rules]
      summary: Create redirect rule
      description: |
        Redirect rules are deeply validated in backend logic. Key constraints include:
        - Source regex parsing and capture group consistency.
        - Destination URL structure and supported placeholder/functions validation.
        - Conditional expression and operator validation.
        - Link-map and destination exclusivity (`linkMapId` requires empty destination).
      operationId: createRedirectRule
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateRedirectRuleRequest'
      responses:
        '200':
          description: Created redirect rule
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RedirectRule'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /api/v1/redirect-rules/analytics:
    get:
      tags: [Redirect Rules]
      summary: Get top redirect rules analytics
      description: Returns aggregated hit statistics for rules within the selected time window.
      operationId: getRedirectRuleAnalytics
      parameters:
        - name: limit
          in: query
          required: false
          description: Number of top rules returned in analytics results.
          schema:
            type: integer
            minimum: 1
            maximum: 50
            default: 50
        - name: range
          in: query
          required: false
          description: Predefined analytics range (used when start/end are not provided).
          schema:
            type: string
            enum: [day, week, month]
        - name: start
          in: query
          required: false
          description: Inclusive start of analytics window (ISO-8601 UTC datetime).
          schema:
            type: string
            format: date-time
        - name: end
          in: query
          required: false
          description: Exclusive end of analytics window (ISO-8601 UTC datetime).
          schema:
            type: string
            format: date-time
      responses:
        '200':
          description: Analytics payload
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RedirectRuleAnalyticsResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /api/v1/redirect-rules/simulate:
    post:
      tags: [Redirect Rules]
      summary: Simulate redirect rule matching
      description: Evaluates request samples against current rules without applying live redirects.
      operationId: simulateRedirectRules
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SimulateRedirectsRequest'
      responses:
        '200':
          description: Simulation results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RedirectSimulationResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /api/v1/redirect-rules/{id}:
    parameters:
      - $ref: '#/components/parameters/EntityId'
    get:
      tags: [Redirect Rules]
      summary: Get redirect rule
      description: Returns one redirect rule by ID.
      operationId: getRedirectRule
      responses:
        '200':
          description: Redirect rule details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RedirectRule'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    put:
      tags: [Redirect Rules]
      summary: Update redirect rule
      description: Updates a redirect rule and re-runs backend validation constraints.
      operationId: updateRedirectRule
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateRedirectRuleRequest'
      responses:
        '200':
          description: Updated redirect rule
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RedirectRule'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    delete:
      tags: [Redirect Rules]
      summary: Delete redirect rule
      description: Soft-deletes a redirect rule so it no longer participates in matching.
      operationId: deleteRedirectRule
      responses:
        '200':
          description: Deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /api/v1/redirect-tests:
    get:
      tags: [Redirect Tests]
      summary: List redirect tests
      description: Lists stored redirect test cases for a domain group with cursor pagination.
      operationId: listRedirectTests
      parameters:
        - name: domainGroupId
          in: query
          required: true
          description: Domain group scope for listing test fixtures.
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Maximum number of tests returned in one page.
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 100
        - name: search
          in: query
          required: false
          description: Optional text search over test path/query content.
          schema:
            type: string
        - name: startAfterId
          in: query
          required: false
          description: Cursor token using the last item ID from previous page.
          schema:
            type: string
      responses:
        '200':
          description: Redirect tests list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RedirectTestQueryResult'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    post:
      tags: [Redirect Tests]
      summary: Create redirect test
      description: Stores a redirect expectation fixture used for simulation-based regression checks.
      operationId: createRedirectTest
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateRedirectTestRequest'
      responses:
        '200':
          description: Created redirect test
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RedirectTest'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /api/v1/redirect-tests/{id}:
    parameters:
      - $ref: '#/components/parameters/EntityId'
    get:
      tags: [Redirect Tests]
      summary: Get redirect test
      description: Returns one stored redirect test fixture by ID.
      operationId: getRedirectTest
      responses:
        '200':
          description: Redirect test details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RedirectTest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    put:
      tags: [Redirect Tests]
      summary: Update redirect test
      description: Updates mutable fields of a stored redirect test fixture.
      operationId: updateRedirectTest
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateRedirectTestRequest'
      responses:
        '200':
          description: Updated redirect test
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RedirectTest'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    delete:
      tags: [Redirect Tests]
      summary: Delete redirect test
      description: Soft-deletes a redirect test fixture.
      operationId: deleteRedirectTest
      responses:
        '200':
          description: Deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /api/v1/link-maps:
    get:
      tags: [Link Maps]
      summary: List link maps
      description: Lists link maps for a domain group, including matching behavior settings.
      operationId: listLinkMaps
      parameters:
        - name: domainGroupId
          in: query
          required: true
          description: Domain group scope for listing link maps.
          schema:
            type: string
      responses:
        '200':
          description: Link maps list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LinkMapQueryResult'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    post:
      tags: [Link Maps]
      summary: Create link map
      description: Creates a link map that can be referenced by redirect rules for key-based resolution.
      operationId: createLinkMap
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateLinkMapRequest'
      responses:
        '200':
          description: Created link map
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LinkMap'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /api/v1/link-maps/{id}:
    parameters:
      - $ref: '#/components/parameters/EntityId'
    get:
      tags: [Link Maps]
      summary: Get link map
      description: Returns one link map by ID.
      operationId: getLinkMap
      responses:
        '200':
          description: Link map details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LinkMap'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    put:
      tags: [Link Maps]
      summary: Update link map
      description: Updates link-map behavior such as case sensitivity, query matching, or fallback destination.
      operationId: updateLinkMap
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateLinkMapRequest'
      responses:
        '200':
          description: Updated link map
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LinkMap'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    delete:
      tags: [Link Maps]
      summary: Delete link map
      description: Soft-deletes a link map. Deletion may be blocked when map is used by active rules.
      operationId: deleteLinkMap
      responses:
        '200':
          description: Deleted
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /api/v1/link-map-entries:
    get:
      tags: [Link Map Entries]
      summary: List link map entries
      description: Lists entries of one link map using cursor pagination and optional search.
      operationId: listLinkMapEntries
      parameters:
        - name: linkMapId
          in: query
          required: true
          description: Parent link-map ID used to scope entry listing.
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Maximum number of entries returned in one page.
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: search
          in: query
          required: false
          description: Optional search string matched against entry keys.
          schema:
            type: string
        - name: startAfterId
          in: query
          required: false
          description: Cursor token using the last item ID from previous page.
          schema:
            type: string
      responses:
        '200':
          description: Link map entries list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LinkMapEntryQueryResult'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    post:
      tags: [Link Map Entries]
      summary: Create link map entry
      description: Creates one key-to-destination mapping in a link map.
      operationId: createLinkMapEntry
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateLinkMapEntryRequest'
      responses:
        '200':
          description: Created link map entry
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LinkMapEntry'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    delete:
      tags: [Link Map Entries]
      summary: Delete multiple link map entries by IDs
      description: Bulk-deletes selected entries in a single request.
      operationId: deleteManyLinkMapEntries
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DeleteLinkMapEntriesByIdRequest'
      responses:
        '200':
          description: Delete summary
          content:
            application/json:
              schema:
                type: object
                properties:
                  deletedCount:
                    type: integer
                required: [deletedCount]
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /api/v1/link-map-entries/import:
    post:
      tags: [Link Map Entries]
      summary: Import link map entries
      description: Bulk upserts entries for a link map and returns import totals plus row-level failures.
      operationId: importLinkMapEntries
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ImportLinkMapEntriesRequest'
      responses:
        '200':
          description: Import summary
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportLinkMapEntriesResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /api/v1/link-map-entries/import/rollback:
    post:
      tags: [Link Map Entries]
      summary: Roll back imported link map entries
      description: Deletes selected imported entries to revert a previous import batch.
      operationId: rollbackLinkMapEntriesImport
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DeleteLinkMapEntriesByIdRequest'
      responses:
        '200':
          description: Rollback summary
          content:
            application/json:
              schema:
                type: object
                properties:
                  deletedCount:
                    type: integer
                required: [deletedCount]
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /api/v1/link-map-entries/{id}:
    parameters:
      - $ref: '#/components/parameters/EntityId'
    get:
      tags: [Link Map Entries]
      summary: Get link map entry
      description: Returns one link-map entry by ID.
      operationId: getLinkMapEntry
      responses:
        '200':
          description: Link map entry details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LinkMapEntry'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    put:
      tags: [Link Map Entries]
      summary: Update link map entry
      description: Updates key or destination for a link-map entry.
      operationId: updateLinkMapEntry
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateLinkMapEntryRequest'
      responses:
        '200':
          description: Updated link map entry
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LinkMapEntry'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    delete:
      tags: [Link Map Entries]
      summary: Delete link map entry
      description: Soft-deletes one link-map entry by ID.
      operationId: deleteLinkMapEntry
      responses:
        '200':
          description: Deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: API key generated in LinkShift dashboard (`Organization -> Manage API keys`).

  parameters:
    EntityId:
      name: id
      in: path
      required: true
      description: Resource identifier.
      schema:
        type: string

  responses:
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            unauthorized:
              value:
                code: 401
                key: unauthorized
                message: Unauthorized
                details: Missing or invalid API key.
                requestId: req_example
    PaymentRequired:
      description: API usage requires paid plan
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            paymentRequired:
              value:
                code: 402
                key: payment_required
                message: Payment required
                feature: api_access
                details: API key usage is available on paid plans only. Upgrade your subscription to use API access.
                requestId: req_example
    TooManyRequests:
      description: Per-key rate limit exceeded
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            tooManyRequests:
              value:
                code: 429
                key: too_many_requests
                message: Too many requests
                details: API key rate limit exceeded
                requestId: req_example
    BadRequest:
      description: Validation error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Conflict:
      description: Resource conflict
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'

  schemas:
    ErrorResponse:
      description: Standardized error payload returned by API endpoints.
      type: object
      properties:
        code:
          type: integer
          description: HTTP-like numeric status code.
        key:
          type: string
          description: Stable machine-readable error key.
        message:
          type: string
          description: Short error summary.
        details:
          type: string
          description: Human-readable details explaining the error.
        requestId:
          type: string
          description: Correlation ID useful for support and troubleshooting.
        feature:
          type: string
          description: Optional feature gate identifier when relevant (for example `api_access`).
      required: [code, key, message]

    DomainGroup:
      description: Domain-group entity used to scope domains, rules, tests, and link maps.
      type: object
      properties:
        id: { type: string, description: Domain-group ID. }
        name: { type: string, description: Human-readable domain-group name. }
        organizationId: { type: string, description: Owning organization ID. }
        robotsPolicy:
          $ref: '#/components/schemas/RobotsPolicy'
        customRobotsContent:
          type: string
          nullable: true
          maxLength: 4096
          description: Used only when robotsPolicy is CUSTOM.
        createdAt: { type: string, format: date-time, description: Creation timestamp (UTC). }
        updatedAt: { type: string, format: date-time, description: Last update timestamp (UTC). }
        deletedAt:
          type: string
          format: date-time
          nullable: true
          description: Soft-delete timestamp in UTC when deleted.
      required: [id, name, organizationId, robotsPolicy, createdAt, updatedAt]

    Domain:
      description: Domain entity assigned to a domain group.
      type: object
      properties:
        id: { type: string, description: Domain ID. }
        name: { type: string, description: Fully qualified domain name. }
        domainGroupId: { type: string, description: Parent domain-group ID. }
        createdAt: { type: string, format: date-time, description: Creation timestamp (UTC). }
        updatedAt: { type: string, format: date-time, description: Last update timestamp (UTC). }
        deletedAt:
          type: string
          format: date-time
          nullable: true
          description: Soft-delete timestamp in UTC when deleted.
      required: [id, name, domainGroupId, createdAt, updatedAt]

    RedirectRule:
      description: Redirect rule resolved during request matching.
      type: object
      properties:
        id: { type: string, description: Redirect-rule ID. }
        source: { type: string, description: Source matcher expression. }
        destination:
          type: string
          nullable: true
          description: Static redirect destination URL. Null when dynamic destination is used via link map.
        statusCode:
          type: integer
          enum: [301, 302, 307, 308]
          description: HTTP redirect status returned for matches.
        matchMethod:
          type: array
          description: Allowed request methods. Empty array means all methods.
          items:
            $ref: '#/components/schemas/HttpMethod'
        queryMatch:
          type: string
          enum: [exact, ignore, subset]
          description: Query-string matching strategy.
        pathMatch:
          type: string
          enum: [exact, prefix]
          description: Path matching strategy.
        linkMapId:
          type: string
          nullable: true
          description: Optional linked map used for key-based destinations.
        isBlocked:
          { type: boolean, description: Whether the rule is currently blocked from execution. }
        blockedAt:
          type: string
          format: date-time
          nullable: true
          description: Timestamp when the rule was blocked.
        priority: { type: integer, description: Rule priority (higher first). }
        domainGroupId: { type: string, description: Parent domain-group ID. }
        createdAt: { type: string, format: date-time, description: Creation timestamp (UTC). }
        updatedAt: { type: string, format: date-time, description: Last update timestamp (UTC). }
        deletedAt:
          type: string
          format: date-time
          nullable: true
          description: Soft-delete timestamp in UTC when deleted.
      required:
        - id
        - source
        - statusCode
        - matchMethod
        - queryMatch
        - pathMatch
        - priority
        - domainGroupId
        - createdAt
        - updatedAt

    RedirectTest:
      description: Stored redirect test fixture used for regression checks.
      type: object
      properties:
        id: { type: string, description: Redirect-test ID. }
        organizationId: { type: string, description: Owning organization ID. }
        domainGroupId: { type: string, description: Domain-group scope for this test. }
        pathWithQuery: { type: string, description: Request path including query string. }
        requestData:
          type: object
          additionalProperties: true
          description: Optional request metadata such as method, headers, query, protocol, or user-agent.
        expectedResult:
          type: object
          additionalProperties: true
          description: Expected simulation outcome payload.
        createdAt: { type: string, format: date-time, description: Creation timestamp (UTC). }
        updatedAt: { type: string, format: date-time, description: Last update timestamp (UTC). }
        deletedAt:
          type: string
          format: date-time
          nullable: true
          description: Soft-delete timestamp in UTC when deleted.
      required:
        - id
        - organizationId
        - domainGroupId
        - pathWithQuery
        - requestData
        - expectedResult
        - createdAt
        - updatedAt

    LinkMap:
      description: Link map used to resolve short keys to destinations.
      type: object
      properties:
        id: { type: string, description: Link-map ID. }
        name: { type: string, description: Human-readable link-map name. }
        domainGroupId: { type: string, description: Parent domain-group ID. }
        caseSensitive: { type: boolean, description: Controls key matching case sensitivity. }
        queryMatch:
          type: string
          enum: [exact, ignore, subset]
          description: Query-string matching strategy for key resolution requests.
        fallbackDestination:
          type: string
          nullable: true
          description: Optional destination used when no key matches.
        createdAt: { type: string, format: date-time, description: Creation timestamp (UTC). }
        updatedAt: { type: string, format: date-time, description: Last update timestamp (UTC). }
        deletedAt:
          type: string
          format: date-time
          nullable: true
          description: Soft-delete timestamp in UTC when deleted.
      required:
        - id
        - name
        - domainGroupId
        - caseSensitive
        - queryMatch
        - createdAt
        - updatedAt

    LinkMapEntry:
      description: One key-to-destination mapping inside a link map.
      type: object
      properties:
        id: { type: string, description: Link-map entry ID. }
        linkMapId: { type: string, description: Parent link-map ID. }
        key: { type: string, description: User-provided lookup key. }
        keyNormalized: { type: string, description: Backend-normalized key used in matching. }
        destination: { type: string, description: Destination URL for the key. }
        createdAt: { type: string, format: date-time, description: Creation timestamp (UTC). }
        updatedAt: { type: string, format: date-time, description: Last update timestamp (UTC). }
        deletedAt:
          type: string
          format: date-time
          nullable: true
          description: Soft-delete timestamp in UTC when deleted.
      required:
        - id
        - linkMapId
        - key
        - keyNormalized
        - destination
        - createdAt
        - updatedAt

    OrganizationUsage:
      description: Snapshot of current organization resource usage counters.
      type: object
      properties:
        domainGroups: { type: integer, description: Number of active domain groups. }
        domains: { type: integer, description: Number of active domains. }
        rules: { type: integer, description: Number of active redirect rules. }
        tests: { type: integer, description: Number of active redirect tests. }
        users: { type: integer, description: Number of organization members. }
        apiKeys: { type: integer, description: Number of configured organization API keys. }
        linkMaps: { type: integer, description: Number of active link maps. }
        linkMapEntries: { type: integer, description: Number of active link-map entries. }
      required: [domainGroups, domains, rules, tests, users, apiKeys, linkMaps, linkMapEntries]

    QueryResultMeta:
      description: Metadata envelope shared by cursor-paginated query responses.
      type: object
      properties:
        dataType: { type: string, description: Logical item type in the `data` array. }
        hasMore:
          { type: boolean, description: Whether there are additional results after this page. }
        moreStartingAfterId: { type: string, description: Cursor token to request the next page. }
      required: [dataType, hasMore]

    DomainGroupQueryResult:
      description: Paginated domain-group query response.
      allOf:
        - $ref: '#/components/schemas/QueryResultMeta'
        - type: object
          properties:
            data:
              type: array
              description: Domain-group items in current page.
              items:
                $ref: '#/components/schemas/DomainGroup'
          required: [data]

    DomainQueryResult:
      description: Paginated domain query response.
      allOf:
        - $ref: '#/components/schemas/QueryResultMeta'
        - type: object
          properties:
            data:
              type: array
              description: Domain items in current page.
              items:
                $ref: '#/components/schemas/Domain'
          required: [data]

    RedirectRuleQueryResult:
      description: Paginated redirect-rule query response.
      allOf:
        - $ref: '#/components/schemas/QueryResultMeta'
        - type: object
          properties:
            data:
              type: array
              description: Redirect-rule items in current page.
              items:
                $ref: '#/components/schemas/RedirectRule'
          required: [data]

    RedirectTestQueryResult:
      description: Paginated redirect-test query response.
      allOf:
        - $ref: '#/components/schemas/QueryResultMeta'
        - type: object
          properties:
            data:
              type: array
              description: Redirect-test items in current page.
              items:
                $ref: '#/components/schemas/RedirectTest'
          required: [data]

    LinkMapQueryResult:
      description: Paginated link-map query response.
      allOf:
        - $ref: '#/components/schemas/QueryResultMeta'
        - type: object
          properties:
            data:
              type: array
              description: Link-map items in current page.
              items:
                $ref: '#/components/schemas/LinkMap'
          required: [data]

    LinkMapEntryQueryResult:
      description: Paginated link-map-entry query response.
      allOf:
        - $ref: '#/components/schemas/QueryResultMeta'
        - type: object
          properties:
            data:
              type: array
              description: Link-map-entry items in current page.
              items:
                $ref: '#/components/schemas/LinkMapEntry'
          required: [data]

    HttpMethod:
      description: HTTP request method.
      type: string
      enum: [GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD]

    RobotsPolicy:
      type: string
      description: |
        Controls robots.txt behavior for all domains in a domain group.
        CUSTOM requires customRobotsContent.
      default: NONE
      enum: [NONE, ALLOW_ALL, DISALLOW_ALL, DISALLOW_BAD_BOTS, CUSTOM]

    CreateDomainGroupRequest:
      description: Payload for creating a domain group.
      type: object
      properties:
        name: { type: string, minLength: 1, description: Domain-group name. }
        robotsPolicy:
          $ref: '#/components/schemas/RobotsPolicy'
        customRobotsContent:
          type: string
          nullable: true
          maxLength: 4096
          description: Required when robotsPolicy is CUSTOM.
      required: [name]

    UpdateDomainGroupRequest:
      description: Payload for updating a domain group.
      type: object
      properties:
        name: { type: string, minLength: 1, description: Domain-group name. }
        robotsPolicy:
          $ref: '#/components/schemas/RobotsPolicy'
        customRobotsContent:
          type: string
          nullable: true
          maxLength: 4096
          description: Required when robotsPolicy is CUSTOM.
      required: [name]

    CreateDomainRequest:
      description: Payload for creating a domain.
      type: object
      properties:
        name: { type: string, description: Fully qualified domain name. }
        domainGroupId: { type: string, description: Target domain-group ID. }
      required: [name, domainGroupId]

    UpdateDomainRequest:
      description: Payload for updating a domain.
      type: object
      properties:
        name: { type: string, description: Fully qualified domain name. }
        domainGroupId: { type: string, description: Target domain-group ID. }

    CreateRedirectRuleRequest:
      description: Payload for creating a redirect rule.
      type: object
      properties:
        source:
          { type: string, minLength: 1, maxLength: 16384, description: Source matcher expression. }
        destination:
          type: string
          maxLength: 16384
          nullable: true
          description: Static destination URL. Keep null when using `linkMapId`.
        statusCode:
          type: integer
          enum: [301, 302, 307, 308]
          default: 302
          description: HTTP redirect status used for rule matches.
        matchMethod:
          type: array
          description: Allowed request methods. Empty array means all methods.
          items:
            $ref: '#/components/schemas/HttpMethod'
        queryMatch:
          type: string
          enum: [exact, ignore, subset]
          default: exact
          description: Query-string matching strategy.
        pathMatch:
          type: string
          enum: [exact, prefix]
          default: exact
          description: Path matching strategy.
        linkMapId:
          type: string
          nullable: true
          description: Optional link-map ID for key-based destinations.
        priority:
          type: integer
          minimum: 0
          maximum: 1000
          default: 0
          description: Rule priority (higher first).
        domainGroupId: { type: string, description: Target domain-group ID. }
      required: [source, domainGroupId]

    UpdateRedirectRuleRequest:
      description: Payload for updating a redirect rule.
      type: object
      properties:
        source:
          { type: string, minLength: 1, maxLength: 16384, description: Source matcher expression. }
        destination:
          type: string
          maxLength: 16384
          nullable: true
          description: Static destination URL. Keep null when using `linkMapId`.
        statusCode:
          type: integer
          enum: [301, 302, 307, 308]
          description: HTTP redirect status used for rule matches.
        matchMethod:
          type: array
          description: Allowed request methods. Empty array means all methods.
          items:
            $ref: '#/components/schemas/HttpMethod'
        queryMatch:
          type: string
          enum: [exact, ignore, subset]
          description: Query-string matching strategy.
        pathMatch: { type: string, enum: [exact, prefix], description: Path matching strategy. }
        linkMapId:
          type: string
          nullable: true
          description: Optional link-map ID for key-based destinations.
        priority:
          { type: integer, minimum: 0, maximum: 1000, description: Rule priority (higher first). }

    SimulateRedirectsRequest:
      description: Batch request payload for rule simulation.
      type: object
      properties:
        entries:
          type: array
          minItems: 1
          maxItems: 100
          description: Request samples that should be evaluated by the simulation engine.
          items:
            type: object
            properties:
              domainGroupId: { type: string, description: Domain-group scope for this sample. }
              hostname: { type: string, description: Optional request host header. }
              path: { type: string, description: Request path (without protocol/host). }
              method:
                $ref: '#/components/schemas/HttpMethod'
              protocol:
                type: string
                enum: [http, https]
                description: Request protocol used in matching context.
              ip: { type: string, description: Optional client IP used by conditional rules. }
              userAgent:
                { type: string, description: Optional user-agent used by conditional rules. }
              headers:
                type: object
                description: Optional request headers map.
                additionalProperties:
                  type: string
              query:
                type: object
                description: Optional query parameters map.
                additionalProperties: true
            required: [domainGroupId, path]
      required: [entries]

    RedirectSimulationResponse:
      description: Simulation result payload for each input request sample.
      type: object
      properties:
        results:
          type: array
          description: Simulation result rows matching the input order.
          items:
            type: object
            properties:
              index: { type: integer, description: Index of input entry. }
              domainGroupId: { type: string, description: Domain-group scope used in simulation. }
              method: { type: string, description: Effective HTTP method used for matching. }
              path: { type: string, description: Effective request path used for matching. }
              hostname: { type: string, description: Effective host used for matching. }
              matched: { type: boolean, description: Whether a redirect rule matched. }
              statusCode: { type: integer, description: Resulting HTTP status code. }
              target:
                type: string
                nullable: true
                description: Resolved redirect target URL when matched.
            required: [index, domainGroupId, method, path, hostname, matched, statusCode]
      required: [results]

    RedirectRuleAnalyticsResponse:
      description: Aggregated rule-level traffic analytics.
      type: object
      properties:
        data:
          type: array
          description: Per-rule analytics entries.
          items:
            type: object
            properties:
              rule:
                $ref: '#/components/schemas/RedirectRule'
              hits:
                type: integer
                description: Total matched hits in selected range.
              topLinkMapKeys:
                type: array
                description: Most frequent link-map keys matched by this rule.
                items:
                  type: object
                  properties:
                    key: { type: string, description: Resolved link-map key. }
                    hits: { type: integer, description: Number of hits for this key. }
                  required: [key, hits]
              topRequestVariants:
                type: array
                description: Most frequent request variant signatures for this rule.
                items:
                  type: object
                  properties:
                    requestMethod: { type: string, description: Request method. }
                    requestPath: { type: string, description: Request path. }
                    requestQuery: { type: string, description: Request query string. }
                    requestUrl: { type: string, description: Full normalized request URL. }
                    destination:
                      { type: string, description: Destination URL returned by matching. }
                    linkMapKey:
                      type: string
                      nullable: true
                      description: Link-map key used in resolution when applicable.
                    hits: { type: integer, description: Hit count for this variant. }
                  required:
                    [requestMethod, requestPath, requestQuery, requestUrl, destination, hits]
            required: [rule, hits, topLinkMapKeys, topRequestVariants]
      required: [data]

    CreateRedirectTestRequest:
      description: Payload for creating a redirect test fixture.
      type: object
      properties:
        domainGroupId: { type: string, description: Domain-group scope for the test. }
        pathWithQuery:
          type: string
          minLength: 1
          maxLength: 16384
          description: Request path including query string.
        requestData:
          type: object
          description: Optional request context map (method, protocol, query, headers, IP, user-agent).
          additionalProperties: true
        expectedResult:
          type: object
          description: Expected simulation outcome.
          properties:
            matched: { type: boolean, description: Expected match/no-match decision. }
            statusCode:
              { type: integer, minimum: 100, maximum: 599, description: Expected HTTP status code. }
            target:
              { type: string, nullable: true, description: Expected destination URL (or null). }
          required: [matched, statusCode, target]
      required: [domainGroupId, pathWithQuery, expectedResult]

    UpdateRedirectTestRequest:
      description: Payload for updating a redirect test fixture.
      type: object
      properties:
        pathWithQuery:
          type: string
          minLength: 1
          maxLength: 16384
          description: Request path including query string.
        requestData:
          type: object
          description: Optional request context map (method, protocol, query, headers, IP, user-agent).
          additionalProperties: true
        expectedResult:
          type: object
          description: Expected simulation outcome.
          properties:
            matched: { type: boolean, description: Expected match/no-match decision. }
            statusCode:
              { type: integer, minimum: 100, maximum: 599, description: Expected HTTP status code. }
            target:
              { type: string, nullable: true, description: Expected destination URL (or null). }
          required: [matched, statusCode, target]

    CreateLinkMapRequest:
      description: Payload for creating a link map.
      type: object
      properties:
        name: { type: string, minLength: 1, maxLength: 120, description: Link-map display name. }
        domainGroupId: { type: string, description: Parent domain-group ID. }
        caseSensitive:
          { type: boolean, default: false, description: Whether key matching is case sensitive. }
        queryMatch:
          type: string
          enum: [exact, ignore, subset]
          default: ignore
          description: Query matching strategy for key resolution requests.
        fallbackDestination:
          { type: string, description: Fallback destination URL used when key lookup misses. }
      required: [name, domainGroupId]

    UpdateLinkMapRequest:
      description: Payload for updating a link map.
      type: object
      properties:
        name: { type: string, minLength: 1, maxLength: 120, description: Link-map display name. }
        caseSensitive: { type: boolean, description: Whether key matching is case sensitive. }
        queryMatch:
          type: string
          enum: [exact, ignore, subset]
          description: Query matching strategy for key resolution requests.
        fallbackDestination:
          type: string
          nullable: true
          description: Fallback destination URL used when key lookup misses.

    CreateLinkMapEntryRequest:
      description: Payload for creating one link-map entry.
      type: object
      properties:
        linkMapId: { type: string, description: Parent link-map ID. }
        key: { type: string, minLength: 1, maxLength: 1024, description: Lookup key. }
        destination:
          type: string
          minLength: 1
          maxLength: 16384
          description: Destination URL resolved for the key.
      required: [linkMapId, key, destination]

    UpdateLinkMapEntryRequest:
      description: Payload for updating one link-map entry.
      type: object
      properties:
        key: { type: string, minLength: 1, maxLength: 1024, description: Lookup key. }
        destination:
          type: string
          minLength: 1
          maxLength: 16384
          description: Destination URL resolved for the key.

    DeleteLinkMapEntriesByIdRequest:
      description: Payload for bulk deletion of link-map entries.
      type: object
      properties:
        linkMapId: { type: string, description: Parent link-map ID. }
        entryIds:
          type: array
          minItems: 1
          maxItems: 1000
          description: Entry IDs that should be deleted.
          items: { type: string }
      required: [linkMapId, entryIds]

    ImportLinkMapEntriesRequest:
      description: Payload for bulk import/upsert of link-map entries.
      type: object
      properties:
        linkMapId: { type: string, description: Parent link-map ID. }
        entries:
          type: array
          minItems: 1
          maxItems: 500
          description: Entries to create or update.
          items:
            type: object
            properties:
              key: { type: string, minLength: 1, maxLength: 1024, description: Lookup key. }
              destination:
                type: string
                minLength: 1
                maxLength: 16384
                description: Destination URL resolved for the key.
            required: [key, destination]
      required: [linkMapId, entries]

    ImportLinkMapEntriesResponse:
      description: Import summary including totals and row-level failures.
      type: object
      properties:
        total: { type: integer, description: Number of processed rows. }
        created: { type: integer, description: Number of newly created entries. }
        updated: { type: integer, description: Number of updated entries. }
        failed: { type: integer, description: Number of rows rejected during import. }
        failures:
          type: array
          description: Row-level failures with index, key, and reason.
          items:
            type: object
            properties:
              index: { type: integer, description: Index of failed row in import payload. }
              key: { type: string, description: Entry key from failed row. }
              reason: { type: string, description: Validation or processing failure reason. }
            required: [index, key, reason]
      required: [total, created, updated, failed, failures]
