openapi: 3.1.0
info:
  title: SSA Agent API
  version: 1.4.0
  description: REST API for AI agents and automation clients. Read endpoints support all backends; submit supports Google Sheets and Airtable. Form create/configure require agent:write.
servers:
  - url: '{serverUrl}'
    variables:
      serverUrl:
        default: https://sheetentry.com
security:
  - bearerAuth: []
paths:
  /v2/agent/forms:
    get:
      summary: List and search forms
      parameters:
        - in: query
          name: q
          schema:
            type: string
        - in: query
          name: formType
          schema:
            type: string
            enum: [google, airtable, onlyoffice, grist, local]
        - in: query
          name: folderId
          schema:
            type: string
        - in: query
          name: limit
          schema:
            type: integer
            default: 50
        - in: query
          name: offset
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: Form summaries
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentFormListResponse'
              examples:
                default:
                  value:
                    data:
                      - id: form1
                        name: Inventory
                        backend: google
                        fieldCount: 2
                        autoSendable: false
                    meta:
                      total: 1
                      limit: 50
                      offset: 0
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/InsufficientScope'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      operationId: createForm
      summary: Create an app Form
      description: >
        Create a Spreadsheet Data Entry Form (template or blank). A destination is
        required: either bind an existing one with backend + backendConfig, or use
        createDestination with provider google or airtable (vault required) to create
        a Spreadsheet/table then bind. Airtable requires an existing baseId (no base
        create). Unbound Forms are not supported. Requires agent:write (PAT) or JWT.
        Idempotency-Key required.
      parameters:
        - in: header
          name: Idempotency-Key
          required: true
          description: 1–128 characters; safe retries for create
          schema:
            type: string
            minLength: 1
            maxLength: 128
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateFormRequest'
            examples:
              blankBindExisting:
                summary: Blank Form bound to an existing Sheet
                value:
                  name: Inventory
                  autoSendable: false
                  fields:
                    - name: SKU
                      column: A
                      type: text
                      required: true
                      autoFill: off
                    - name: Qty
                      column: B
                      type: number
                      required: true
                      autoFill: off
                  backend: google
                  backendConfig:
                    spreadsheetId: 1abcExisting
                    sheetName: Sheet1
              fromTemplateBindExisting:
                summary: From template + bind existing Sheet
                value:
                  templateId: 'https://example.com/sheet|Sheet1|Inventory with QR'
                  name: Warehouse count
                  backend: google
                  backendConfig:
                    spreadsheetId: 1abcExisting
                    sheetName: Sheet1
      responses:
        '201':
          description: Form created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentFormDetail'
        '200':
          description: Idempotent replay of a successful create
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentFormDetail'
        '400':
          $ref: '#/components/responses/ValidationFailed'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/InsufficientScope'
        '409':
          description: Idempotency conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentErrorResponse'
              examples:
                inProgress:
                  value:
                    error:
                      code: idempotency_in_progress
                      message: A request with this Idempotency-Key is already in progress
                keyReused:
                  value:
                    error:
                      code: idempotency_key_reused
                      message: Idempotency-Key was already used for a different request
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalError'
  /v2/agent/forms/{id}:
    get:
      summary: Get form schema
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Agent form schema
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentFormDetail'
              examples:
                default:
                  value:
                    id: form1
                    name: Inventory
                    backend: google
                    backendConfig:
                      spreadsheetId: sheet-123
                      sheetName: Sheet1
                    fields:
                      - name: Product
                        column: A
                        type: text
                        required: true
                        autoFill: off
                      - name: Qty
                        column: B
                        type: number
                        required: false
                        autoFill: currentDate
                    autoSendable: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/InsufficientScope'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalError'
    patch:
      operationId: updateForm
      summary: Configure an app Form
      description: >
        Partial update of name, autoSendable, fields (full replace), backend binding,
        or folder. Empty body {} is 400. Patching backend alone clears backendConfig
        unless backendConfig is also in the same request. When backendConfig is sent,
        it fully replaces the destination binding (not merged); {} clears destination ids.
        Does not support createDestination (create-time only). Requires agent:write (PAT) or JWT.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchFormRequest'
            examples:
              configureFields:
                value:
                  autoSendable: true
                  fields:
                    - name: SKU
                      column: A
                      type: barcode
                      required: true
                      autoFill: off
                    - name: Qty
                      column: B
                      type: number
                      required: true
                      autoFill: off
      responses:
        '200':
          description: Updated Form
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentFormDetail'
        '400':
          $ref: '#/components/responses/ValidationFailed'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/InsufficientScope'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalError'
  /v2/agent/forms/{id}/submit:
    post:
      summary: Submit form values (Google Sheets, Airtable)
      description: |
        Requires provider connection in vault (`PUT /v2/users/connections/google` or `.../airtable`). OnlyOffice, Grist, and Local forms return `unsupported_backend`.

        **Idempotency trade-off:** if the provider send succeeds but persisting the idempotency record fails, the key is released and the client still receives `200`. A retry with the same `Idempotency-Key` may send duplicate data to the provider.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
        - in: header
          name: Idempotency-Key
          required: true
          description: Required. Enables safe retries (24h TTL, scoped per user and form). Use a unique value per intentional submit; reuse the same key only when retrying the same request.
          schema:
            type: string
            minLength: 1
            maxLength: 128
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - type: object
                  required: [values]
                  properties:
                    values:
                      type: object
                      additionalProperties: true
                      description: Map of Form field name to value.
                - type: object
                  required: [valuesEntries]
                  properties:
                    valuesEntries:
                      type: array
                      description: >
                        Alternative row shape: one object per field. Provide exactly
                        one of `values` or `valuesEntries`, not both.
                      items:
                        type: object
                        required: [field, value]
                        properties:
                          field:
                            type: string
                            description: Form field name (matches schema field names).
                          value:
                            description: Field value (stringified server-side).
      responses:
        '200':
          description: Submit succeeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubmitSuccess'
              examples:
                default:
                  value:
                    ok: true
                    formId: form1
                    sentAt: '2026-06-01T00:00:00.000Z'
        '400':
          description: Validation error or unsupported backend
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentErrorResponse'
              examples:
                validationFailed:
                  value:
                    error:
                      code: validation_failed
                      message: Idempotency-Key header is required
                      details:
                        field: Idempotency-Key
                unsupportedBackend:
                  value:
                    error:
                      code: unsupported_backend
                      message: Local forms cannot be submitted via Agent API
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          description: Quota exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentErrorResponse'
              examples:
                default:
                  value:
                    error:
                      code: quota_exceeded
                      message: Free send limit exceeded
        '403':
          description: Provider auth required or insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentErrorResponse'
              examples:
                providerAuthRequired:
                  value:
                    error:
                      code: provider_auth_required
                      message: 'Provider connection required: google'
                      details:
                        provider: google
                insufficientScope:
                  value:
                    error:
                      code: insufficient_scope
                      message: Token does not have the required scope
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Idempotency key conflict or request in progress
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentErrorResponse'
              examples:
                keyReused:
                  value:
                    error:
                      code: idempotency_key_reused
                      message: Idempotency key was used for another form
                      details:
                        formId: form2
                inProgress:
                  value:
                    error:
                      code: idempotency_in_progress
                      message: A request with this idempotency key is already in progress
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
        '502':
          description: Provider send failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentErrorResponse'
              examples:
                default:
                  value:
                    error:
                      code: send_failed
                      message: Provider API error
  /v2/agent/send-history:
    get:
      summary: List server-side send history
      parameters:
        - in: query
          name: formId
          schema:
            type: string
        - in: query
          name: limit
          schema:
            type: integer
            default: 50
      responses:
        '200':
          description: Send history records
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendHistoryListResponse'
              examples:
                default:
                  value:
                    data:
                      - id: hist1
                        formId: form1
                        formName: Inventory
                        backend: google
                        values:
                          Product: Laptop
                        sentAt: '2026-06-01T10:00:00.000Z'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/InsufficientScope'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalError'
  /v2/users/connections/{provider}:
    get:
      summary: Get provider connection status
      parameters:
        - in: path
          name: provider
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Connection status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProviderConnectionStatus'
              examples:
                connected:
                  value:
                    provider: google
                    connected: true
                    expiresAt: null
                    scopes: []
                    updatedAt: '2026-06-01T00:00:00.000Z'
                notConnected:
                  value:
                    provider: google
                    connected: false
        '400':
          $ref: '#/components/responses/ValidationFailed'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
    put:
      summary: Save provider OAuth tokens
      parameters:
        - in: path
          name: provider
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [accessToken]
              properties:
                accessToken:
                  type: string
                refreshToken:
                  type: string
                expiresAt:
                  type: string
                  format: date-time
      responses:
        '200':
          description: Connection saved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProviderConnectionStatus'
              examples:
                default:
                  value:
                    provider: google
                    connected: true
                    expiresAt: null
                    scopes: []
                    updatedAt: '2026-06-01T00:00:00.000Z'
        '400':
          $ref: '#/components/responses/ValidationFailed'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
    delete:
      summary: Delete provider connection
      parameters:
        - in: path
          name: provider
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Deleted
        '400':
          $ref: '#/components/responses/ValidationFailed'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
  /v2/agent/templates:
    get:
      summary: Search template library
      security: []
      parameters:
        - in: query
          name: q
          schema:
            type: string
        - in: query
          name: locale
          schema:
            type: string
            default: en
        - in: query
          name: group
          schema:
            type: string
      responses:
        '200':
          description: Templates and groups
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TemplatesResponse'
              examples:
                default:
                  value:
                    templates:
                      - id: 'https://example.com/sheet|Sheet1|Inventory with QR'
                        templateLink: https://example.com/sheet
                        sheetName: Sheet1
                        groupName: Inventory
                        groupDescription: Inventory templates
                        templateName: Inventory with QR
                        useCase: Track inventory
                        imageUrl: https://example.com/howto.png
                        parameters:
                          - name: Date
                            dataType: Date
                            targetColumn: A
                    groups:
                      - groupName: Inventory
                        groupDescription: Inventory templates
                        templates:
                          - id: 'https://example.com/sheet|Sheet1|Inventory with QR'
                            templateLink: https://example.com/sheet
                            sheetName: Sheet1
                            groupName: Inventory
                            groupDescription: Inventory templates
                            templateName: Inventory with QR
                            useCase: Track inventory
                            imageUrl: https://example.com/howto.png
                            parameters:
                              - name: Date
                                dataType: Date
                                targetColumn: A
        '502':
          $ref: '#/components/responses/BadGateway'
  /v2/agent/folders:
    get:
      summary: List folders
      responses:
        '200':
          description: Folder list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FolderListResponse'
              examples:
                default:
                  value:
                    data:
                      - id: folder1
                        name: Work
                        createdAt: '2026-01-01T00:00:00.000Z'
                        updatedAt: '2026-01-02T00:00:00.000Z'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/InsufficientScope'
        '500':
          $ref: '#/components/responses/InternalError'
  /v2/agent/me:
    get:
      summary: Get current user profile
      responses:
        '200':
          description: Profile and subscription
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentMeProfile'
              examples:
                default:
                  value:
                    id: user1
                    email: user@example.com
                    emailVerified: true
                    language: en
                    isAnonymous: false
                    freeRequests: 10
                    maxFreeRequests: 100
                    createdAt: '2026-01-01T00:00:00.000Z'
                    updatedAt: '2026-01-02T00:00:00.000Z'
                    subscription:
                      startDate: '2026-01-01T00:00:00.000Z'
                      endDate: '2099-01-01T00:00:00.000Z'
                      resumeDate: '2020-01-01T00:00:00.000Z'
                      isActive: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/InsufficientScope'
        '500':
          $ref: '#/components/responses/InternalError'
  /v2/agent/tokens:
    get:
      summary: List personal access tokens
      responses:
        '200':
          description: List of tokens
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/AgentTokenInfo'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/InsufficientScope'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      summary: Create a personal access token
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateTokenRequest'
      responses:
        '201':
          description: Token created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateTokenResponse'
        '400':
          $ref: '#/components/responses/ValidationFailed'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/InsufficientScope'
        '500':
          $ref: '#/components/responses/InternalError'
  /v2/agent/tokens/{id}:
    delete:
      summary: Revoke a personal access token
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Token revoked
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/InsufficientScope'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
  /v2/agent/webhooks:
    get:
      summary: List webhooks
      responses:
        '200':
          description: List of webhooks
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/AgentWebhook'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      summary: Create a webhook
      description: Requires `agent:write` (PAT) or JWT.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateWebhookRequest'
      responses:
        '201':
          description: Webhook created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentWebhook'
        '400':
          $ref: '#/components/responses/ValidationFailed'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/InsufficientScope'
        '500':
          $ref: '#/components/responses/InternalError'
  /v2/agent/webhooks/{id}:
    delete:
      summary: Delete a webhook
      description: Requires `agent:write` (PAT) or JWT.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Webhook deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/InsufficientScope'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
  /v2/agent/openapi:
    get:
      summary: Get OpenAPI specification
      security: []
      parameters:
        - in: query
          name: format
          schema:
            type: string
            enum: [json]
          description: Return parsed OpenAPI as JSON (`Content-Type application/json`). Default is YAML.
      responses:
        '200':
          description: OpenAPI document (YAML by default, or JSON when requested)
          content:
            application/yaml:
              schema:
                type: string
            application/json:
              schema:
                type: object
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT or PAT
      description: |
        Supports two types of tokens:
        1. **JWT**: Obtain via `POST /v2/users/signIn`. Full access.
        2. **PAT**: Personal Access Token starting with `pat_`. Create via `POST /v2/agent/tokens`. Scoped access.
  responses:
    Unauthorized:
      description: Unauthorized
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/AgentErrorResponse'
          examples:
            default:
              value:
                error:
                  code: unauthorized
                  message: Authentication required
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/AgentErrorResponse'
          examples:
            default:
              value:
                error:
                  code: not_found
                  message: Form not found
    ValidationFailed:
      description: Validation error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/AgentErrorResponse'
          examples:
            default:
              value:
                error:
                  code: validation_failed
                  message: accessToken is required
                  details:
                    field: accessToken
    InternalError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/AgentErrorResponse'
          examples:
            default:
              value:
                error:
                  code: internal_error
                  message: An unexpected error occurred
    RateLimitExceeded:
      description: Too many requests
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/AgentErrorResponse'
          examples:
            default:
              value:
                error:
                  code: rate_limit_exceeded
                  message: Too many requests, please try again later.
    InsufficientScope:
      description: Insufficient scope
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/AgentErrorResponse'
          examples:
            default:
              value:
                error:
                  code: insufficient_scope
                  message: Token does not have the required scope
    BadGateway:
      description: Upstream service error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/AgentErrorResponse'
          examples:
            default:
              value:
                error:
                  code: templates_fetch_failed
                  message: Failed to load templates
  schemas:
    AgentError:
      type: object
      required: [code, message]
      properties:
        code:
          type: string
        message:
          type: string
        details:
          type: object
          additionalProperties: true
    AgentErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          $ref: '#/components/schemas/AgentError'
    PaginationMeta:
      type: object
      required: [total, limit, offset]
      properties:
        total:
          type: integer
        limit:
          type: integer
        offset:
          type: integer
    Backend:
      type: string
      enum: [google, airtable, onlyoffice, grist, local]
      nullable: true
    AgentFormSummary:
      type: object
      required: [id, name, backend, fieldCount, autoSendable]
      properties:
        id:
          type: string
        name:
          type: string
        backend:
          $ref: '#/components/schemas/Backend'
        fieldCount:
          type: integer
        autoSendable:
          type: boolean
    AgentFormListResponse:
      type: object
      required: [data, meta]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/AgentFormSummary'
        meta:
          $ref: '#/components/schemas/PaginationMeta'
    AgentField:
      type: object
      required: [name, column, type, required, autoFill]
      properties:
        name:
          type: string
        column:
          type: string
        type:
          type: string
        required:
          type: boolean
        autoFill:
          type: string
    AgentFormDetail:
      type: object
      required: [id, name, backend, backendConfig, fields, autoSendable]
      properties:
        id:
          type: string
        name:
          type: string
        backend:
          $ref: '#/components/schemas/Backend'
        backendConfig:
          type: object
          additionalProperties: true
        fields:
          type: array
          items:
            $ref: '#/components/schemas/AgentField'
        autoSendable:
          type: boolean
        folderId:
          type: string
          nullable: true
          description: Present on write responses; may be omitted on GET until aligned
    PatchFormRequest:
      type: object
      description: >
        At least one property required (empty {} → 400). fields replaces the full list
        when present. backendConfig is a full replace of destination binding (not a merge);
        {} clears destination ids. Patching backend alone clears backendConfig unless
        backendConfig is also sent in the same request.
      minProperties: 1
      properties:
        name:
          type: string
        autoSendable:
          type: boolean
        fields:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/AgentField'
        backend:
          $ref: '#/components/schemas/Backend'
        backendConfig:
          type: object
          additionalProperties: true
          description: Full replace of destination binding; empty object clears ids.
        folderId:
          type: string
          nullable: true
      additionalProperties: false
    CreateDestinationGoogle:
      type: object
      required: [provider]
      properties:
        provider:
          type: string
          const: google
        spreadsheetTitle:
          type: string
          description: Defaults to Form name or Spreadsheet
        sheetName:
          type: string
          default: Sheet1
      additionalProperties: false
    CreateDestinationAirtable:
      type: object
      required: [provider, baseId]
      properties:
        provider:
          type: string
          const: airtable
        baseId:
          type: string
          description: Existing Airtable base id. Creating a base is not supported.
        tableName:
          type: string
          description: Defaults to Form name
      additionalProperties: false
    CreateDestination:
      oneOf:
        - $ref: '#/components/schemas/CreateDestinationGoogle'
        - $ref: '#/components/schemas/CreateDestinationAirtable'
      discriminator:
        propertyName: provider
        mapping:
          google: '#/components/schemas/CreateDestinationGoogle'
          airtable: '#/components/schemas/CreateDestinationAirtable'
      description: >
        Create Form destination then bind. Google and Airtable are live (vault required).
        Airtable requires baseId; no Airtable base create. If destination create succeeds
        but Form persist fails, a retry with the same Idempotency-Key and request body
        resumes from the created Sheet/table (no second Sheet/table, no repeated provider
        call) and only retries the Form persist; a retry with a different body returns
        409 idempotency_key_reused.
    CreateFormRequest:
      type: object
      description: >
        Exactly one create mode: templateId (from template) OR name+fields (blank).
        Do not send templateId and fields together. Destination is required: exactly
        one of backendConfig (bind existing) XOR createDestination. Unbound Forms are
        not supported. Optional folderId. createDestination.provider google and
        airtable are live (vault required).
      properties:
        templateId:
          type: string
          description: >
            Id from GET /v2/agent/templates (pipe-delimited composite id as returned today)
        name:
          type: string
          description: Required for blank create; optional override when using templateId
        autoSendable:
          type: boolean
          default: false
        fields:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/AgentField'
          description: Required for blank create. Forbidden when templateId is set.
        backend:
          $ref: '#/components/schemas/Backend'
        backendConfig:
          type: object
          additionalProperties: true
          description: Bind existing destination; mutually exclusive with createDestination
        createDestination:
          $ref: '#/components/schemas/CreateDestination'
        folderId:
          type: string
          nullable: true
      additionalProperties: false
    SubmitSuccess:
      type: object
      required: [ok, formId, sentAt]
      properties:
        ok:
          type: boolean
          const: true
        formId:
          type: string
        sentAt:
          type: string
          format: date-time
    AgentSubscription:
      type: object
      nullable: true
      required: [startDate, endDate, resumeDate, isActive]
      properties:
        startDate:
          type: string
          format: date-time
        endDate:
          type: string
          format: date-time
        resumeDate:
          type: string
          format: date-time
        isActive:
          type: boolean
    AgentMeProfile:
      type: object
      required:
        - id
        - email
        - emailVerified
        - language
        - isAnonymous
        - freeRequests
        - maxFreeRequests
        - createdAt
        - updatedAt
        - subscription
      properties:
        id:
          type: string
        email:
          type: string
        emailVerified:
          type: boolean
        language:
          type: string
        isAnonymous:
          type: boolean
        freeRequests:
          type: integer
        maxFreeRequests:
          type: integer
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        subscription:
          $ref: '#/components/schemas/AgentSubscription'
    AgentFolder:
      type: object
      required: [id, name, createdAt, updatedAt]
      properties:
        id:
          type: string
        name:
          type: string
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    FolderListResponse:
      type: object
      required: [data]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/AgentFolder'
    SendHistoryRecord:
      type: object
      required: [id, formId, formName, backend, values, sentAt]
      properties:
        id:
          type: string
        formId:
          type: string
        formName:
          type: string
        backend:
          $ref: '#/components/schemas/Backend'
        values:
          type: object
          additionalProperties: true
        sentAt:
          type: string
          format: date-time
    SendHistoryListResponse:
      type: object
      required: [data]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/SendHistoryRecord'
    TemplateParameter:
      type: object
      required: [name, dataType, targetColumn]
      properties:
        name:
          type: string
        dataType:
          type: string
        targetColumn:
          type: string
    AgentTemplate:
      type: object
      required:
        - id
        - templateLink
        - sheetName
        - groupName
        - groupDescription
        - templateName
        - useCase
        - imageUrl
        - parameters
      properties:
        id:
          type: string
        templateLink:
          type: string
        sheetName:
          type: string
        groupName:
          type: string
        groupDescription:
          type: string
        templateName:
          type: string
        useCase:
          type: string
        imageUrl:
          type: string
        parameters:
          type: array
          items:
            $ref: '#/components/schemas/TemplateParameter'
    TemplateGroup:
      type: object
      required: [groupName, groupDescription, templates]
      properties:
        groupName:
          type: string
        groupDescription:
          type: string
        templates:
          type: array
          items:
            $ref: '#/components/schemas/AgentTemplate'
    TemplatesResponse:
      type: object
      required: [templates, groups]
      properties:
        templates:
          type: array
          items:
            $ref: '#/components/schemas/AgentTemplate'
        groups:
          type: array
          items:
            $ref: '#/components/schemas/TemplateGroup'
    ProviderConnectionStatus:
      type: object
      required: [provider, connected]
      properties:
        provider:
          type: string
        connected:
          type: boolean
        expiresAt:
          type: string
          format: date-time
          nullable: true
        scopes:
          type: array
          items:
            type: string
        updatedAt:
          type: string
          format: date-time
    AgentTokenInfo:
      type: object
      required: [id, name, prefix, scopes, createdAt]
      properties:
        id:
          type: string
        name:
          type: string
        prefix:
          type: string
        scopes:
          type: array
          items:
            type: string
        expiresAt:
          type: string
          format: date-time
          nullable: true
        lastUsedAt:
          type: string
          format: date-time
          nullable: true
        createdAt:
          type: string
          format: date-time
    CreateTokenRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
        scopes:
          type: array
          items:
            type: string
            enum: [agent:read, agent:submit, agent:write]
          default: [agent:read, agent:submit]
        expiresAt:
          type: string
          format: date-time
          nullable: true
    AgentWebhook:
      type: object
      required: [id, url, events, createdAt]
      properties:
        id:
          type: string
        url:
          type: string
          format: uri
        events:
          type: array
          items:
            type: string
        createdAt:
          type: string
          format: date-time
    CreateWebhookRequest:
      type: object
      required: [url]
      properties:
        url:
          type: string
          format: uri
        secret:
          type: string
          description: Optional secret for HMAC-SHA256 signing
        events:
          type: array
          items:
            type: string
          default: [form.submitted]
    WebhookPayload:
      type: object
      required: [event, sentAt, payload]
      properties:
        event:
          type: string
          example: form.submitted
        sentAt:
          type: string
          format: date-time
        payload:
          type: object
          properties:
            formId:
              type: string
            values:
              type: object
              additionalProperties: true
    CreateTokenResponse:
      type: object
      required: [id, token, name, scopes, createdAt]
      properties:
        id:
          type: string
        token:
          type: string
          description: The full token string (only shown once upon creation).
        name:
          type: string
        scopes:
          type: array
          items:
            type: string
        expiresAt:
          type: string
          format: date-time
          nullable: true
        createdAt:
          type: string
          format: date-time
