openapi: 3.1.0
info:
  title: SSN Client API
  version: 1.0.0
  summary: Client-facing API for Site Services Now
  description: |
    Tenant-scoped REST API for Site Services Now.

    Current documented/implemented focus:
    - OAuth2 client credentials authentication
    - Sites
    - Site Visits
    - Messages
    - Message attachments
    - Webhook subscriptions

    Site asset assignment is part of the Site Visits resource. Use
    `site-visits:write` to submit `siteAssetIds` and `site-visits:read`
    to read `assetsToService` and `siteVisitAssets`; there are no separate
    client-facing site asset scopes.

    Client API requests are limited to 100 requests per minute. The API does
    not currently provide bulk-create endpoints; large imports should send one
    resource per POST request with throttling, a unique Idempotency-Key per
    record, and retry backoff for 429 or transient upstream errors.
servers:
  - url: https://api.siteservicesnow.com
security:
  - bearerAuth: []
tags:
  - name: Auth
  - name: Sites
  - name: Site Visits
  - name: Reference Data
  - name: Messages
  - name: Webhooks
paths:
  /api/v1/oauth/token:
    post:
      tags: [Auth]
      security: []
      summary: Issue an access token
      description: Exchange client credentials for a short-lived bearer token.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TokenRequest'
            examples:
              basic:
                value:
                  client_id: demo-client
                  client_secret: demo-secret
                  grant_type: client_credentials
                  scope: sites:read sites:write site-visits:read site-visits:write
      responses:
        '200':
          description: Token issued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TokenResponse'
              examples:
                basic:
                  value:
                    access_token: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
                    token_type: Bearer
                    expires_in: 3600
                    scope: sites:read sites:write site-visits:read site-visits:write
        '400':
          $ref: '#/components/responses/Problem'
        '401':
          $ref: '#/components/responses/Problem'
  /api/v1/sites:
    get:
      tags: [Sites]
      summary: List sites
      description: |
        List sites owned by the authenticated client.

        To resolve a known client-side identifier to an SSN site ID, provide one exact-match
        filter: `siteCode`, `uniqueID`, or `uniqueID2`.
      parameters:
        - $ref: '#/components/parameters/PageAfter'
        - $ref: '#/components/parameters/PageSize'
        - in: query
          name: siteCode
          required: false
          schema:
            type: string
          description: Exact-match lookup by the preferred stable client site identifier.
        - in: query
          name: uniqueID
          required: false
          schema:
            type: string
          description: Exact-match lookup by legacy unique ID.
        - in: query
          name: uniqueID2
          required: false
          schema:
            type: string
          description: Exact-match lookup by secondary legacy unique ID.
      responses:
        '200':
          description: Sites list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SiteListResponse'
              examples:
                basic:
                  $ref: '#/components/examples/SiteListResponseExample'
        '401':
          $ref: '#/components/responses/Problem'
        '403':
          $ref: '#/components/responses/Problem'
        '422':
          $ref: '#/components/responses/Problem'
    post:
      tags: [Sites]
      summary: Create site
      description: |
        Create a site for the authenticated client.

        `siteCode` is required and must be unique per client.
        The ownership field is derived from the authenticated client and cannot be supplied by the caller.
        Bulk site imports must send one site per POST request. Client API
        requests are limited to 100 requests per minute.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SiteCreateRequest'
            examples:
              basic:
                value:
                  name: Starbucks - 3rd Ave
                  siteCode: SEA-10027
                  addressLine1: 123 3rd Ave
                  city: Seattle
                  stateRegion: WA
                  postalCode: "98101"
                  country: US
                  uniqueID: KIOSK-12
                  keyRequired: false
      responses:
        '201':
          description: Site created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SiteResponse'
              examples:
                basic:
                  $ref: '#/components/examples/SiteResponseExample'
        '401':
          $ref: '#/components/responses/Problem'
        '403':
          $ref: '#/components/responses/Problem'
        '409':
          description: Duplicate site for the same client
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
              examples:
                duplicateSite:
                  $ref: '#/components/examples/DuplicateSiteProblemExample'
        '422':
          description: Validation failure
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
              examples:
                validation:
                  $ref: '#/components/examples/ValidationProblemExample'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
  /api/v1/projects:
    get:
      tags: [Reference Data]
      summary: List valid projects
      description: |
        List active projects owned by the authenticated client.

        Use this endpoint to discover valid `projectId` values before creating
        site visits. Requires the `site-visits:read` scope.

        This is a capped lookup list. Use `page[size]` to limit the response;
        cursor pagination (`page[after]` and `meta.nextCursor`) is not supported.
      parameters:
        - $ref: '#/components/parameters/PageSize'
      responses:
        '200':
          description: Project reference list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectListResponse'
              examples:
                basic:
                  value:
                    data:
                      - id: "42"
                        type: projects
                        attributes:
                          projectId: 42
                          projectName: ACME Refresh
                          status: Active
                        meta:
                          quickbaseRecordId: "42"
                    meta:
                      pageSize: 25
        '401':
          $ref: '#/components/responses/Problem'
        '403':
          $ref: '#/components/responses/Problem'
        '422':
          $ref: '#/components/responses/Problem'
  /api/v1/items:
    get:
      tags: [Reference Data]
      summary: List valid items
      description: |
        List item names owned by the authenticated client.

        Use this endpoint to discover valid `itemName` values before creating
        site visits. Requires the `site-visits:read` scope.

        This is a capped lookup list. Use `page[size]` to limit the response;
        cursor pagination (`page[after]` and `meta.nextCursor`) is not supported.
      parameters:
        - $ref: '#/components/parameters/PageSize'
      responses:
        '200':
          description: Item reference list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ItemListResponse'
              examples:
                basic:
                  value:
                    data:
                      - id: NCR Self Checkout Gen 2
                        type: items
                        attributes:
                          itemId: 7
                          itemName: NCR Self Checkout Gen 2
                        meta:
                          quickbaseRecordId: "7"
                    meta:
                      pageSize: 25
        '401':
          $ref: '#/components/responses/Problem'
        '403':
          $ref: '#/components/responses/Problem'
        '422':
          $ref: '#/components/responses/Problem'
  /api/v1/sites/{id}:
    get:
      tags: [Sites]
      summary: Get site
      parameters:
        - $ref: '#/components/parameters/Id'
      responses:
        '200':
          description: Site
          headers:
            ETag:
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SiteResponse'
              examples:
                basic:
                  $ref: '#/components/examples/SiteResponseExample'
        '401':
          $ref: '#/components/responses/Problem'
        '403':
          $ref: '#/components/responses/Problem'
        '404':
          $ref: '#/components/responses/Problem'
    patch:
      tags: [Sites]
      summary: Patch site
      parameters:
        - $ref: '#/components/parameters/Id'
        - $ref: '#/components/parameters/IfMatch'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SitePatchRequest'
      responses:
        '200':
          description: Site updated
          headers:
            ETag:
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SiteResponse'
              examples:
                patched:
                  $ref: '#/components/examples/SitePatchedResponseExample'
        '401':
          $ref: '#/components/responses/Problem'
        '403':
          $ref: '#/components/responses/Problem'
        '404':
          $ref: '#/components/responses/Problem'
        '409':
          $ref: '#/components/responses/Problem'
        '422':
          $ref: '#/components/responses/Problem'
  /api/v1/site-visits:
    get:
      tags: [Site Visits]
      summary: List site visits
      description: |
        List site visits for the authenticated client.

        If `clientTicketNumber` is supplied, the API performs a tenant-scoped lookup and returns the matching site visit, if any.
      parameters:
        - $ref: '#/components/parameters/PageAfter'
        - $ref: '#/components/parameters/PageSize'
        - in: query
          name: clientTicketNumber
          required: false
          schema:
            type: string
      responses:
        '200':
          description: Site visits list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SiteVisitListResponse'
              examples:
                basic:
                  $ref: '#/components/examples/SiteVisitListResponseExample'
        '401':
          $ref: '#/components/responses/Problem'
        '403':
          $ref: '#/components/responses/Problem'
    post:
      tags: [Site Visits]
      summary: Create site visit
      description: |
        Create a site visit for an owned site.

        The caller supplies `clientSiteId`, not a raw Quickbase site record ID.
        The API resolves the client-owned site internally before writing the site visit.

        `projectId` must belong to the authenticated client and be `Active`.
        `itemName` must belong to the authenticated client.
        `clientTicketNumber` is optional and lets the client send its own ticket reference.
        If supplied, `clientTicketNumber` must be unique per client.
        `ticketStatus` is read-only and defaults to `New` on creation.
        `returnLabel` uploads happen through the dedicated return-label endpoint, not this request.
        Bulk site-visit imports must send one site visit per POST request.
        Client API requests are limited to 100 requests per minute. For imports
        of 1000 or more records, throttle below that limit and use a unique
        Idempotency-Key for every POST.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SiteVisitCreateRequest'
            examples:
              basic:
                value:
                  clientSiteId: SEA-10027
                  description: Replace damaged kiosk screen
                  clientTicketNumber: CLIENT-TKT-10027
                  serviceType: Break / Fix
                  jobInstructions: Contact store manager on arrival
                  projectId: 1442
                  itemName: NCR Self Checkout Gen 2
                  startDate: 2026-03-30T16:00:00Z
                  dueDate: 2026-03-31T01:00:00Z
                  arePartsRequired: true
                  trackingNumber: 1Z999AA10123456784
                  partsReturnRequired: true
                  schedulingRule: Date/Time Specific
      responses:
        '201':
          description: Site visit created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SiteVisitResponse'
              examples:
                basic:
                  $ref: '#/components/examples/SiteVisitResponseExample'
        '401':
          $ref: '#/components/responses/Problem'
        '403':
          $ref: '#/components/responses/Problem'
        '404':
          description: Related site not found or client does not own it
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
              examples:
                unknownClientSite:
                  $ref: '#/components/examples/UnknownClientSiteProblemExample'
        '409':
          description: Invalid project or concurrency conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
              examples:
                invalidProject:
                  $ref: '#/components/examples/InvalidProjectProblemExample'
        '422':
          description: Validation failure
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
              examples:
                validation:
                  $ref: '#/components/examples/ValidationProblemExample'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
  /api/v1/site-visits/{id}:
    get:
      tags: [Site Visits]
      summary: Get site visit
      parameters:
        - $ref: '#/components/parameters/Id'
      responses:
        '200':
          description: Site visit
          headers:
            ETag:
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SiteVisitResponse'
              examples:
                basic:
                  $ref: '#/components/examples/SiteVisitResponseExample'
        '401':
          $ref: '#/components/responses/Problem'
        '403':
          $ref: '#/components/responses/Problem'
        '404':
          $ref: '#/components/responses/Problem'
    patch:
      tags: [Site Visits]
      summary: Patch site visit
      parameters:
        - $ref: '#/components/parameters/Id'
        - $ref: '#/components/parameters/IfMatch'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SiteVisitPatchRequest'
      responses:
        '200':
          description: Site visit updated
          headers:
            ETag:
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SiteVisitResponse'
              examples:
                patched:
                  $ref: '#/components/examples/SiteVisitPatchedResponseExample'
        '401':
          $ref: '#/components/responses/Problem'
        '403':
          $ref: '#/components/responses/Problem'
        '404':
          $ref: '#/components/responses/Problem'
        '409':
          $ref: '#/components/responses/Problem'
        '422':
          $ref: '#/components/responses/Problem'
  /api/v1/site-visits/{id}/return-label:
    post:
      tags: [Site Visits]
      summary: Upload latest return label
      description: Upload a PDF return label for an owned site visit. Each upload creates a new version.
      parameters:
        - $ref: '#/components/parameters/Id'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
      responses:
        '201':
          description: Return label uploaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReturnLabelUploadResponse'
        '401':
          $ref: '#/components/responses/Problem'
        '403':
          $ref: '#/components/responses/Problem'
        '404':
          $ref: '#/components/responses/Problem'
        '415':
          $ref: '#/components/responses/Problem'
        '422':
          $ref: '#/components/responses/Problem'
    get:
      tags: [Site Visits]
      summary: Download latest return label
      parameters:
        - $ref: '#/components/parameters/Id'
      responses:
        '200':
          description: Latest return label PDF
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        '401':
          $ref: '#/components/responses/Problem'
        '403':
          $ref: '#/components/responses/Problem'
        '404':
          $ref: '#/components/responses/Problem'
  /api/v1/messages:
    get:
      tags: [Messages]
      summary: List messages
      description: List messages owned by the authenticated client.
      parameters:
        - $ref: '#/components/parameters/PageAfter'
        - $ref: '#/components/parameters/PageSize'
      responses:
        '200':
          description: Messages list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageListResponse'
              examples:
                basic:
                  $ref: '#/components/examples/MessageListResponseExample'
        '401':
          $ref: '#/components/responses/Problem'
        '403':
          $ref: '#/components/responses/Problem'
    post:
      tags: [Messages]
      summary: Create message
      description: |
        Create a message related to an owned site visit.

        The caller supplies `siteVisitId` as the SSN API site-visit ID.
        The API resolves it to the owned Quickbase site-visit record internally.
        Clients can write `type`, `messageThread`, `status`, `urgency`, `buyer`, and `clientContacts`.
        The API sets `method`, `occurredAt`, and `ssnPm` internally.
        Bulk message imports must send one message per POST request. Client API
        requests are limited to 100 requests per minute.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MessageCreateRequest'
            examples:
              basic:
                value:
                  siteVisitId: visit_fbe3df2526c3444cb7c809c5ec917c12
                  type: General
                  messageThread: Store manager confirmed access for the scheduled window.
                  status: Open
                  urgency: Normal – Standard priority / routine handling
                  buyer: true
                  clientContacts: true
      responses:
        '201':
          description: Message created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageResponse'
              examples:
                basic:
                  $ref: '#/components/examples/MessageResponseExample'
        '401':
          $ref: '#/components/responses/Problem'
        '403':
          $ref: '#/components/responses/Problem'
        '404':
          description: Related site visit not found or client does not own it
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
        '422':
          $ref: '#/components/responses/Problem'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
  /api/v1/messages/{id}:
    get:
      tags: [Messages]
      summary: Get message
      parameters:
        - $ref: '#/components/parameters/Id'
      responses:
        '200':
          description: Message
          headers:
            ETag:
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageResponse'
              examples:
                basic:
                  $ref: '#/components/examples/MessageResponseExample'
        '401':
          $ref: '#/components/responses/Problem'
        '403':
          $ref: '#/components/responses/Problem'
        '404':
          $ref: '#/components/responses/Problem'
    patch:
      tags: [Messages]
      summary: Patch message
      parameters:
        - $ref: '#/components/parameters/Id'
        - $ref: '#/components/parameters/IfMatch'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MessagePatchRequest'
      responses:
        '200':
          description: Message updated
          headers:
            ETag:
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageResponse'
              examples:
                basic:
                  $ref: '#/components/examples/MessageResponseExample'
        '401':
          $ref: '#/components/responses/Problem'
        '403':
          $ref: '#/components/responses/Problem'
        '404':
          $ref: '#/components/responses/Problem'
        '409':
          $ref: '#/components/responses/Problem'
        '422':
          $ref: '#/components/responses/Problem'
  /api/v1/messages/{id}/file-attachment:
    post:
      tags: [Messages]
      summary: Upload latest message attachment
      description: Upload a PDF, PNG, or JPEG attachment for an owned message. Each upload creates a new version.
      parameters:
        - $ref: '#/components/parameters/Id'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
      responses:
        '201':
          description: Message attachment uploaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageAttachmentUploadResponse'
        '401':
          $ref: '#/components/responses/Problem'
        '403':
          $ref: '#/components/responses/Problem'
        '404':
          $ref: '#/components/responses/Problem'
        '415':
          $ref: '#/components/responses/Problem'
        '422':
          $ref: '#/components/responses/Problem'
    get:
      tags: [Messages]
      summary: Download latest message attachment
      parameters:
        - $ref: '#/components/parameters/Id'
      responses:
        '200':
          description: Latest message attachment file
          content:
            application/pdf:
              schema:
                type: string
                format: binary
            image/png:
              schema:
                type: string
                format: binary
            image/jpeg:
              schema:
                type: string
                format: binary
        '401':
          $ref: '#/components/responses/Problem'
        '403':
          $ref: '#/components/responses/Problem'
        '404':
          $ref: '#/components/responses/Problem'
  /api/v1/webhook-subscriptions:
    get:
      tags: [Webhooks]
      summary: List webhook subscriptions
      responses:
        '200':
          description: Webhook subscriptions
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookSubscriptionListResponse'
              examples:
                basic:
                  $ref: '#/components/examples/WebhookSubscriptionListResponseExample'
    post:
      tags: [Webhooks]
      summary: Create webhook subscription
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookSubscriptionCreateRequest'
      responses:
        '201':
          description: Subscription created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookSubscriptionResponse'
              examples:
                basic:
                  $ref: '#/components/examples/WebhookSubscriptionResponseExample'
  /api/v1/webhook-subscriptions/{id}:
    patch:
      tags: [Webhooks]
      summary: Patch webhook subscription
      parameters:
        - $ref: '#/components/parameters/Id'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookSubscriptionPatchRequest'
      responses:
        '200':
          description: Subscription updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookSubscriptionResponse'
              examples:
                basic:
                  $ref: '#/components/examples/WebhookSubscriptionResponseExample'
components:
  parameters:
    Id:
      in: path
      name: id
      required: true
      schema:
        type: string
    PageAfter:
      in: query
      name: page[after]
      required: false
      schema:
        type: string
    PageSize:
      in: query
      name: page[size]
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 25
    IdempotencyKey:
      in: header
      name: Idempotency-Key
      required: false
      schema:
        type: string
    IfMatch:
      in: header
      name: If-Match
      required: false
      schema:
        type: string
  responses:
    Problem:
      description: Problem details error response
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ProblemDetails'
    RateLimitExceeded:
      description: Too many requests. Client API requests are limited to 100 requests per minute.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ProblemDetails'
          examples:
            rateLimitExceeded:
              value:
                type: https://api.ssn.local/problems/rate-limit-exceeded
                title: Too Many Requests
                status: 429
                detail: Client API requests are limited to 100 requests per minute.
  schemas:
    TokenRequest:
      type: object
      required: [client_id, client_secret, grant_type]
      properties:
        client_id:
          type: string
        client_secret:
          type: string
        grant_type:
          type: string
          const: client_credentials
        scope:
          type: string
    TokenResponse:
      type: object
      required: [access_token, token_type, expires_in, scope]
      properties:
        access_token:
          type: string
        token_type:
          type: string
          const: Bearer
        expires_in:
          type: integer
        scope:
          type: string
    ProblemDetails:
      type: object
      required: [type, title, status, detail]
      properties:
        type:
          type: string
        title:
          type: string
        status:
          type: integer
        detail:
          type: string
        errors:
          type: object
          additionalProperties:
            type: array
            items:
              type: string
    SiteAttributes:
      type: object
      required: [clientId, status, createdAt, updatedAt, name, siteCode, addressLine1, city, stateRegion, postalCode, country]
      properties:
        clientId:
          type: string
        status:
          type: string
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        name:
          type: string
        siteCode:
          type: string
          description: Unique client site identifier. Must be unique per client.
        addressLine1:
          type: string
        addressLine2:
          type: string
        city:
          type: string
        stateRegion:
          type: string
        postalCode:
          type: string
        country:
          type: string
        uniqueID:
          type: string
        uniqueID2:
          type: string
        friendlyLocationName:
          type: string
        friendlyLocationName2:
          type: string
        keyRequired:
          type: boolean
    SiteCreateRequest:
      type: object
      required: [name, siteCode, addressLine1, city, stateRegion, postalCode, country]
      properties:
        name:
          type: string
        siteCode:
          type: string
        addressLine1:
          type: string
        addressLine2:
          type: string
        city:
          type: string
        stateRegion:
          type: string
        postalCode:
          type: string
        country:
          type: string
        uniqueID:
          type: string
        uniqueID2:
          type: string
        friendlyLocationName:
          type: string
        friendlyLocationName2:
          type: string
        keyRequired:
          type: boolean
      additionalProperties: false
    SitePatchRequest:
      allOf:
        - $ref: '#/components/schemas/SiteCreateRequest'
      required: []
    SiteResource:
      type: object
      required: [id, type, attributes, meta]
      properties:
        id:
          type: string
        type:
          type: string
          const: sites
        attributes:
          $ref: '#/components/schemas/SiteAttributes'
        meta:
          $ref: '#/components/schemas/ResourceMeta'
    SiteResponse:
      type: object
      required: [data]
      properties:
        data:
          $ref: '#/components/schemas/SiteResource'
    SiteListResponse:
      type: object
      required: [data, meta]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/SiteResource'
        meta:
          $ref: '#/components/schemas/ListMeta'
    ReferenceResourceMeta:
      type: object
      properties:
        quickbaseRecordId:
          type: string
    ProjectAttributes:
      type: object
      required: [projectId, projectName, status]
      properties:
        projectId:
          oneOf:
            - type: string
            - type: integer
        projectName:
          type: string
        status:
          type: string
          const: Active
    ProjectResource:
      type: object
      required: [id, type, attributes, meta]
      properties:
        id:
          type: string
        type:
          type: string
          const: projects
        attributes:
          $ref: '#/components/schemas/ProjectAttributes'
        meta:
          $ref: '#/components/schemas/ReferenceResourceMeta'
    ProjectListResponse:
      type: object
      required: [data, meta]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/ProjectResource'
        meta:
          $ref: '#/components/schemas/ListMeta'
    ItemAttributes:
      type: object
      required: [itemId, itemName]
      properties:
        itemId:
          oneOf:
            - type: string
            - type: integer
        itemName:
          type: string
    ItemResource:
      type: object
      required: [id, type, attributes, meta]
      properties:
        id:
          type: string
        type:
          type: string
          const: items
        attributes:
          $ref: '#/components/schemas/ItemAttributes'
        meta:
          $ref: '#/components/schemas/ReferenceResourceMeta'
    ItemListResponse:
      type: object
      required: [data, meta]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/ItemResource'
        meta:
          $ref: '#/components/schemas/ListMeta'
    SiteVisitServiceType:
      type: string
      enum:
        - Regular Service
        - Preventative Maintenance
        - Bonus
        - Break / Fix
        - Survey
        - Site Prep
        - Installation
        - Relocation
        - Removal
        - Swap
        - Audit
        - Storage
        - W/G Service
        - Transport
        - Machine Prep
        - Sign/Wrap
        - Inventory
        - Delivery Acceptance
        - Delivery/Inventory
        - Install Revisit
        - Secret Shopper
        - Recycling
        - Training
    SiteVisitSchedulingRule:
      type: string
      enum:
        - Date/Time Specific
        - Date Specific
        - Date Range
    SiteVisitAttributes:
      type: object
      required: [clientId, status, createdAt, updatedAt, clientSiteId, description, serviceType, jobInstructions, projectId, itemName, startDate, dueDate, arePartsRequired, schedulingRule]
      properties:
        clientId:
          type: string
        status:
          type: string
          description: Current ticket status from Quickbase.
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        clientSiteId:
          type: string
          description: Client-owned site identifier resolved to the underlying site record internally.
        description:
          type: string
        clientTicketNumber:
          type: string
          description: Optional client-supplied job reference or external ticket number.
        serviceType:
          $ref: '#/components/schemas/SiteVisitServiceType'
        jobInstructions:
          type: string
        projectId:
          oneOf:
            - type: integer
            - type: string
          description: Client-owned project Quickbase record ID. Must currently be Active.
        itemName:
          type: string
          description: Client-owned item name. Must be unique per client.
        startDate:
          type: string
          format: date-time
        dueDate:
          type: string
          format: date-time
        arePartsRequired:
          type: boolean
        trackingNumber:
          type: string
        partsReturnRequired:
          type: boolean
        assetsToService:
          type: string
          description: Combined text summary of the assigned site assets.
        siteVisitAssets:
          type: array
          description: Assigned site asset child rows returned under Site Visits read scope.
          items:
            $ref: '#/components/schemas/SiteVisitAsset'
        schedulingRule:
          $ref: '#/components/schemas/SiteVisitSchedulingRule'
        returnLabel:
          $ref: '#/components/schemas/ReturnLabel'
    SiteVisitCreateRequest:
      type: object
      required:
        - clientSiteId
        - description
        - serviceType
        - jobInstructions
        - projectId
        - itemName
        - startDate
        - dueDate
        - arePartsRequired
        - schedulingRule
      properties:
        clientSiteId:
          type: string
        description:
          type: string
        clientTicketNumber:
          type: string
        serviceType:
          $ref: '#/components/schemas/SiteVisitServiceType'
        jobInstructions:
          type: string
        projectId:
          oneOf:
            - type: integer
            - type: string
        itemName:
          type: string
        startDate:
          type: string
          format: date-time
        dueDate:
          type: string
          format: date-time
        arePartsRequired:
          type: boolean
        trackingNumber:
          type: string
        partsReturnRequired:
          type: boolean
        siteAssetIds:
          type: array
          description: Site asset record IDs to assign to the visit. Requires Site Visits write scope; site assets do not have separate client-facing scopes.
          items:
            type: integer
        schedulingRule:
          $ref: '#/components/schemas/SiteVisitSchedulingRule'
      additionalProperties: false
    SiteVisitPatchRequest:
      allOf:
        - $ref: '#/components/schemas/SiteVisitCreateRequest'
      required: []
    SiteVisitAsset:
      type: object
      required: [siteVisitAssetId, siteAssetId, uniqueId, friendlyLocation, assetStatus, assetDisplay]
      properties:
        siteVisitAssetId:
          type: integer
        siteAssetId:
          type: integer
        uniqueId:
          type: string
        friendlyLocation:
          type: string
        assetStatus:
          type: string
        assetDisplay:
          type: string
    SiteVisitResource:
      type: object
      required: [id, type, attributes, meta]
      properties:
        id:
          type: string
        type:
          type: string
          const: site-visits
        attributes:
          $ref: '#/components/schemas/SiteVisitAttributes'
        meta:
          $ref: '#/components/schemas/ResourceMeta'
    SiteVisitResponse:
      type: object
      required: [data]
      properties:
        data:
          $ref: '#/components/schemas/SiteVisitResource'
    SiteVisitListResponse:
      type: object
      required: [data, meta]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/SiteVisitResource'
        meta:
          $ref: '#/components/schemas/ListMeta'
    MessageAttributes:
      type: object
      required: [clientId, status, createdAt, updatedAt, siteVisitId, messageThread]
      properties:
        clientId:
          type: string
        status:
          type: string
          description: Current message status from Quickbase.
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        siteVisitId:
          type: string
          description: SSN API site-visit identifier for the related ticket.
        type:
          $ref: '#/components/schemas/MessageType'
        recordOwner:
          description: Raw Quickbase Record Owner value.
        messageThread:
          type: string
        fileAttachment:
          $ref: '#/components/schemas/MessageAttachment'
        urgency:
          $ref: '#/components/schemas/MessageUrgency'
        buyer:
          type: boolean
        clientContacts:
          type: boolean
        ssnPm:
          type: boolean
    MessageType:
      type: string
      enum:
        - Scope Update
        - Change Appointment
        - Cancellation
        - Job On-Hold
        - Tracking Update
        - Floor Plan
        - Other
        - Reminder
        - Due Date Change Request
        - Missed Appointment
        - Incomplete Scope
        - Site Prep Issue
        - MFG/Hardware Issues
        - Shipping Damage
        - Pricing Estimate
        - Warranty Request
        - Exception
        - Parts Missing
        - Schedule Please
        - Billing Dispute
        - Request Travel Pay
        - General
        - Schedule
        - Pay
        - Support
        - Request
    MessageUrgency:
      type: string
      enum:
        - Low – No immediate action required
        - Normal – Standard priority / routine handling
        - High – Needs attention soon
        - Critical – Immediate action required
    MessageCreateRequest:
      type: object
      required: [siteVisitId, messageThread]
      properties:
        siteVisitId:
          type: string
        type:
          $ref: '#/components/schemas/MessageType'
        messageThread:
          type: string
        status:
          type: string
          enum: [Open, Closed]
        urgency:
          $ref: '#/components/schemas/MessageUrgency'
        buyer:
          type: boolean
        clientContacts:
          type: boolean
      additionalProperties: false
    MessagePatchRequest:
      type: object
      properties:
        type:
          $ref: '#/components/schemas/MessageType'
        messageThread:
          type: string
        status:
          type: string
          enum: [Open, Closed]
        urgency:
          $ref: '#/components/schemas/MessageUrgency'
        buyer:
          type: boolean
        clientContacts:
          type: boolean
      additionalProperties: false
    MessageResource:
      type: object
      required: [id, type, attributes, meta]
      properties:
        id:
          type: string
        type:
          type: string
          const: messages
        attributes:
          $ref: '#/components/schemas/MessageAttributes'
        meta:
          $ref: '#/components/schemas/ResourceMeta'
    MessageResponse:
      type: object
      required: [data]
      properties:
        data:
          $ref: '#/components/schemas/MessageResource'
    MessageListResponse:
      type: object
      required: [data, meta]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/MessageResource'
        meta:
          $ref: '#/components/schemas/ListMeta'
    MessageAttachmentVersion:
      type: object
      required: [version, fileName, contentType, sizeBytes, uploadedAt]
      properties:
        version:
          type: integer
        fileName:
          type: string
        contentType:
          type: string
        sizeBytes:
          type: integer
        uploadedAt:
          type: string
          format: date-time
    MessageAttachment:
      type: object
      required: [url, versions]
      properties:
        url:
          type: string
        versions:
          type: array
          items:
            $ref: '#/components/schemas/MessageAttachmentVersion'
    MessageAttachmentUploadPayload:
      type: object
      required: [messageId, fileAttachment]
      properties:
        messageId:
          type: string
        fileAttachment:
          $ref: '#/components/schemas/MessageAttachment'
    MessageAttachmentUploadResponse:
      type: object
      required: [data]
      properties:
        data:
          $ref: '#/components/schemas/MessageAttachmentUploadPayload'
    ReturnLabelVersion:
      type: object
      required: [version, fileName, contentType, sizeBytes, uploadedAt]
      properties:
        version:
          type: integer
        fileName:
          type: string
        contentType:
          type: string
        sizeBytes:
          type: integer
        uploadedAt:
          type: string
          format: date-time
    ReturnLabel:
      type: object
      required: [url, versions]
      properties:
        url:
          type: string
        versions:
          type: array
          items:
            $ref: '#/components/schemas/ReturnLabelVersion'
    ReturnLabelUploadPayload:
      type: object
      required: [siteVisitId, returnLabel]
      properties:
        siteVisitId:
          type: string
        returnLabel:
          $ref: '#/components/schemas/ReturnLabel'
    ReturnLabelUploadResponse:
      type: object
      required: [data]
      properties:
        data:
          $ref: '#/components/schemas/ReturnLabelUploadPayload'
    WebhookSubscriptionCreateRequest:
      type: object
      required: [targetUrl, eventTypes, secret]
      properties:
        targetUrl:
          type: string
          format: uri
        description:
          type: string
        eventTypes:
          type: array
          description: |
            Supported site-visit webhook event types. `ticket.created` can be emitted for client API-created site visits and SSN-created or registered site visits outside the API. The other lifecycle events originate from SSN/Quickbase workflows and are delivered only when webhook access is enabled and the client is API-enabled through the `SSN API Enabled` setting.
          items:
            type: string
            enum:
              - ticket.created
              - rep.assigned
              - job.scheduled
              - schedule.updated
              - job.dispatched
              - job.completed
              - deliverables.transmitted
        secret:
          type: string
    WebhookSubscriptionPatchRequest:
      type: object
      properties:
        targetUrl:
          type: string
          format: uri
        description:
          type: string
        eventTypes:
          type: array
          description: |
            Supported site-visit webhook event types. `ticket.created` can be emitted for client API-created site visits and SSN-created or registered site visits outside the API. The other lifecycle events originate from SSN/Quickbase workflows and are delivered only when webhook access is enabled and the client is API-enabled through the `SSN API Enabled` setting.
          items:
            type: string
            enum:
              - ticket.created
              - rep.assigned
              - job.scheduled
              - schedule.updated
              - job.dispatched
              - job.completed
              - deliverables.transmitted
        secret:
          type: string
        isActive:
          type: boolean
    WebhookSubscription:
      type: object
      required: [id, tenantId, targetUrl, eventTypes, isActive, createdAt, updatedAt]
      properties:
        id:
          type: string
        tenantId:
          type: string
        targetUrl:
          type: string
        description:
          type: string
        eventTypes:
          type: array
          description: |
            Supported site-visit webhook event types. `ticket.created` can be emitted for client API-created site visits and SSN-created or registered site visits outside the API. The other lifecycle events originate from SSN/Quickbase workflows and are delivered only when webhook access is enabled and the client is API-enabled through the `SSN API Enabled` setting.
          items:
            type: string
            enum:
              - ticket.created
              - rep.assigned
              - job.scheduled
              - schedule.updated
              - job.dispatched
              - job.completed
              - deliverables.transmitted
        isActive:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    WebhookSubscriptionResponse:
      type: object
      required: [data]
      properties:
        data:
          $ref: '#/components/schemas/WebhookSubscription'
    WebhookSubscriptionListResponse:
      type: object
      required: [data]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/WebhookSubscription'
    ResourceMeta:
      type: object
      required: [version, etag]
      properties:
        version:
          type: integer
        etag:
          type: string
    ListMeta:
      type: object
      required: [pageSize]
      properties:
        nextCursor:
          type: string
        pageSize:
          type: integer
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
  examples:
    SiteResponseExample:
      value:
        data:
          id: site_95f81d2c53c64cf3adfef72f72c8fb16
          type: sites
          attributes:
            clientId: tenant-acme-retail
            status: unknown
            createdAt: '2026-03-24T22:10:11.000Z'
            updatedAt: '2026-03-24T22:10:11.000Z'
            name: Starbucks - 3rd Ave
            siteCode: SEA-10027
            addressLine1: 123 3rd Ave
            addressLine2: Suite 140
            city: Seattle
            stateRegion: WA
            postalCode: '98101'
            country: US
            uniqueID: KIOSK-12
            uniqueID2: ASSET-77821
            friendlyLocationName: Near the 2nd floor elevators
            friendlyLocationName2: Across from customer service
            keyRequired: false
          meta:
            version: 1
            etag: '"1"'
    SitePatchedResponseExample:
      value:
        data:
          id: site_95f81d2c53c64cf3adfef72f72c8fb16
          type: sites
          attributes:
            clientId: tenant-acme-retail
            status: unknown
            createdAt: '2026-03-24T22:11:43.000Z'
            updatedAt: '2026-03-24T22:11:43.000Z'
            name: Starbucks - 3rd Ave
            siteCode: SEA-10027
            addressLine1: 123 3rd Ave
            addressLine2: Suite 140
            city: Seattle
            stateRegion: WA
            postalCode: '98101'
            country: US
            uniqueID: KIOSK-12
            uniqueID2: ASSET-77821
            friendlyLocationName: Inside vestibule near kiosk bank
            friendlyLocationName2: Across from customer service
            keyRequired: true
          meta:
            version: 2
            etag: '"2"'
    SiteListResponseExample:
      value:
        data:
          - id: site_95f81d2c53c64cf3adfef72f72c8fb16
            type: sites
            attributes:
              clientId: tenant-acme-retail
              status: unknown
              createdAt: '2026-03-24T22:10:11.000Z'
              updatedAt: '2026-03-24T22:10:11.000Z'
              name: Starbucks - 3rd Ave
              siteCode: SEA-10027
              addressLine1: 123 3rd Ave
              city: Seattle
              stateRegion: WA
              postalCode: '98101'
              country: US
            meta:
              version: 1
              etag: '"1"'
        meta:
          pageSize: 25
    SiteVisitResponseExample:
      value:
        data:
          id: visit_fbe3df2526c3444cb7c809c5ec917c12
          type: site-visits
          attributes:
            clientId: tenant-acme-retail
            status: New
            createdAt: '2026-03-24T22:15:02.000Z'
            updatedAt: '2026-03-24T22:15:02.000Z'
            clientSiteId: SEA-10027
            description: Replace damaged kiosk screen
            clientTicketNumber: CLIENT-TKT-10027
            serviceType: Break / Fix
            jobInstructions: Check in with store manager before opening the equipment.
            projectId: 1442
            itemName: NCR Self Checkout Gen 2
            startDate: '2026-03-30T16:00:00Z'
            dueDate: '2026-03-31T01:00:00Z'
            arePartsRequired: true
            trackingNumber: 1Z999AA10123456784
            partsReturnRequired: true
            assetsToService: TestUnit0002 - Entry Vestibule; TestUnit0001 - Backroom
            siteVisitAssets:
              - siteVisitAssetId: 123
                siteAssetId: 457
                uniqueId: TestUnit0002
                friendlyLocation: Entry Vestibule
                assetStatus: Active
                assetDisplay: TestUnit0002 - Entry Vestibule
              - siteVisitAssetId: 124
                siteAssetId: 456
                uniqueId: TestUnit0001
                friendlyLocation: Backroom
                assetStatus: Active
                assetDisplay: TestUnit0001 - Backroom
            schedulingRule: Date/Time Specific
          meta:
            version: 1
            etag: '"1"'
    SiteVisitPatchedResponseExample:
      value:
        data:
          id: visit_fbe3df2526c3444cb7c809c5ec917c12
          type: site-visits
          attributes:
            clientId: tenant-acme-retail
            status: Scheduled
            createdAt: '2026-03-24T22:20:41.000Z'
            updatedAt: '2026-03-24T22:20:41.000Z'
            clientSiteId: SEA-10027
            description: Replace damaged kiosk screen
            clientTicketNumber: CLIENT-TKT-10027-REV2
            serviceType: Break / Fix
            jobInstructions: Call district manager if the store manager is unavailable.
            projectId: 1442
            itemName: NCR Self Checkout Gen 2
            startDate: '2026-03-30T16:00:00Z'
            dueDate: '2026-03-31T01:00:00Z'
            arePartsRequired: true
            trackingNumber: 1Z999AA10123456799
            partsReturnRequired: false
            assetsToService: TestUnit0002 - Entry Vestibule
            siteVisitAssets:
              - siteVisitAssetId: 123
                siteAssetId: 457
                uniqueId: TestUnit0002
                friendlyLocation: Entry Vestibule
                assetStatus: Active
                assetDisplay: TestUnit0002 - Entry Vestibule
            schedulingRule: Date/Time Specific
          meta:
            version: 2
            etag: '"2"'
    SiteVisitListResponseExample:
      value:
        data:
          - id: visit_fbe3df2526c3444cb7c809c5ec917c12
            type: site-visits
            attributes:
              clientId: tenant-acme-retail
              status: Scheduled
              createdAt: '2026-03-24T22:20:41.000Z'
              updatedAt: '2026-03-24T22:20:41.000Z'
              clientSiteId: SEA-10027
              description: Replace damaged kiosk screen
              clientTicketNumber: CLIENT-TKT-10027-REV2
              serviceType: Break / Fix
              jobInstructions: Call district manager if the store manager is unavailable.
              projectId: 1442
              itemName: NCR Self Checkout Gen 2
              startDate: '2026-03-30T16:00:00Z'
              dueDate: '2026-03-31T01:00:00Z'
              arePartsRequired: true
              trackingNumber: 1Z999AA10123456799
              partsReturnRequired: false
              assetsToService: TestUnit0002 - Entry Vestibule
              schedulingRule: Date/Time Specific
            meta:
              version: 2
              etag: '"2"'
        meta:
          pageSize: 25
    MessageResponseExample:
      value:
        data:
          id: msg_5fd995a6018d4f0d8b4e48ef4e640f67
          type: messages
          attributes:
            clientId: tenant-acme-retail
            status: Open
            createdAt: '2026-04-03T16:00:01.000Z'
            updatedAt: '2026-04-03T16:00:01.000Z'
            siteVisitId: visit_fbe3df2526c3444cb7c809c5ec917c12
            type: General
            recordOwner: owner@example.com
            messageThread: Store manager confirmed access for the scheduled window.
            fileAttachment:
              url: https://api.siteservicesnow.com/api/v1/messages/msg_5fd995a6018d4f0d8b4e48ef4e640f67/file-attachment
              versions:
                - version: 1
                  fileName: message-proof.pdf
                  contentType: application/pdf
                  sizeBytes: 182344
                  uploadedAt: '2026-04-03T17:55:00.000Z'
            urgency: Normal – Standard priority / routine handling
            buyer: true
            clientContacts: true
            ssnPm: true
          meta:
            version: 1
            etag: '"1"'
    MessageListResponseExample:
      value:
        data:
          - id: msg_5fd995a6018d4f0d8b4e48ef4e640f67
            type: messages
            attributes:
              clientId: tenant-acme-retail
              status: Open
              createdAt: '2026-04-03T16:00:01.000Z'
              updatedAt: '2026-04-03T16:00:01.000Z'
              siteVisitId: visit_fbe3df2526c3444cb7c809c5ec917c12
              type: General
              recordOwner: owner@example.com
              messageThread: Store manager confirmed access for the scheduled window.
              fileAttachment:
                url: https://api.siteservicesnow.com/api/v1/messages/msg_5fd995a6018d4f0d8b4e48ef4e640f67/file-attachment
                versions:
                  - version: 1
                    fileName: message-proof.pdf
                    contentType: application/pdf
                    sizeBytes: 182344
                    uploadedAt: '2026-04-03T17:55:00.000Z'
              urgency: Normal – Standard priority / routine handling
              buyer: true
              clientContacts: true
              ssnPm: true
            meta:
              version: 1
              etag: '"1"'
        meta:
          pageSize: 25
    WebhookSubscriptionResponseExample:
      value:
        data:
          id: whsub_653d7b3137f2464da99de7f4638aa4f9
          tenantId: tenant-acme-retail
          targetUrl: https://client.example.com/ssn/webhooks
          description: Production SSN webhook endpoint
          eventTypes:
            - ticket.created
            - rep.assigned
            - job.scheduled
            - schedule.updated
            - job.dispatched
            - job.completed
            - deliverables.transmitted
          isActive: true
          createdAt: '2026-03-24T22:23:11.000Z'
          updatedAt: '2026-03-24T22:23:11.000Z'
    WebhookSubscriptionListResponseExample:
      value:
        data:
          - id: whsub_653d7b3137f2464da99de7f4638aa4f9
            tenantId: tenant-acme-retail
            targetUrl: https://client.example.com/ssn/webhooks
            description: Production SSN webhook endpoint
            eventTypes:
              - ticket.created
              - rep.assigned
              - job.scheduled
              - schedule.updated
              - job.dispatched
              - job.completed
              - deliverables.transmitted
            isActive: true
            createdAt: '2026-03-24T22:23:11.000Z'
            updatedAt: '2026-03-24T22:23:11.000Z'
    DuplicateSiteProblemExample:
      value:
        type: https://api.ssn.local/problems/duplicate-resource
        title: Duplicate Resource
        status: 409
        detail: sites with siteCode "SEA-10027" already exists for this client.
    InvalidProjectProblemExample:
      value:
        type: https://api.ssn.local/problems/invalid-project
        title: Invalid Project
        status: 409
        detail: Project 1442 is not Active.
    UnknownClientSiteProblemExample:
      value:
        type: https://api.ssn.local/problems/not-found
        title: Not Found
        status: 404
        detail: No owned site matched the provided clientSiteId.
    ValidationProblemExample:
      value:
        type: https://api.ssn.local/problems/validation-error
        title: Validation Error
        status: 422
        detail: Request validation failed.
        errors:
          siteCode:
            - Required
