openapi: 3.1.0
info:
  title: Clipform API
  description: |
    Build and manage interactive video-style forms. Used by the Clipform ChatGPT integration.

    ## Rate limiting

    Every response carries the IETF-draft RateLimit header fields so clients can
    self-throttle: `RateLimit-Limit` (the window quota), `RateLimit-Remaining`
    (requests left), and `RateLimit-Reset` (seconds until the window resets). A
    throttled request returns `429` with a `Retry-After` header (seconds to wait).

    ## Versioning and deprecation

    The API is versioned in the URL path (`/v1`). Additive, backward-compatible
    changes (new endpoints, new optional fields) ship within a version without a
    version bump. Breaking changes ship under a new path prefix (`/v2`). When an
    endpoint is deprecated it is marked `deprecated` in this specification and its
    responses carry a `Deprecation` header and a `Sunset` header (RFC 8594) giving
    the date after which it may stop responding - at least 90 days out.
  version: 1.0.0
  contact:
    name: Clipform
    url: https://clipform.io
    email: support@clipform.io
  termsOfService: https://clipform.io/terms
servers:
  - url: https://api.clipform.io/v1
    description: Production
    x-internal: false

tags:
  - name: Forms
    x-displayName: Forms
    description: Create, read, update, and delete forms.
  - name: Nodes
    x-displayName: Nodes
    description: Add, edit, and remove the nodes inside a form, including routing logic.
  - name: node-media
    x-displayName: Node Media
    description: Attach and manage the video or image media on a node.
  - name: Tags
    x-displayName: Tags
    description: Organise forms with workspace tags.
  - name: Analytics
    x-displayName: Analytics
    description: Read funnel analytics, submissions, and results for a form.
  - name: tracked-links
    x-displayName: Tracked Links
    description: Create and manage tracked share links with click-through analytics.
  - name: Webhooks
    x-displayName: Webhooks
    description: The form.completed event Clipform POSTs to your endpoint when a respondent completes a form.
  - name: Authentication
    x-displayName: Authentication
    description: Verify the caller identity, workspace, and plan.
  - name: Integrations
    x-displayName: Integrations
    description: The connect lane an installed app uses on a user's behalf - identify the workspace, list its forms, and subscribe to form completions. Used by the Clipform apps for Zapier, Make and n8n.
paths:
  /me:
    get:
      operationId: getMe
      tags:
        - Authentication
      summary: Get current user
      description: Returns the authenticated caller's workspace and plan limits.
      responses:
        "200":
          description: Identity + plan summary
          content:
            application/json:
              schema:
                type: object
                properties:
                  auth_mode:
                    type: string
                    enum: [session, api_key, anonymous]
                  user_id:
                    type: string
                    format: uuid
                    nullable: true
                    description: Present in session mode only.
                    maxLength: 36
                  workspace:
                    type: object
                    nullable: true
                    properties:
                      id:
                        type: string
                        format: uuid
                        maxLength: 36
                      name:
                        type: string
                        maxLength: 2000
                      role:
                        type: string
                        description: Caller's role in the workspace. Session mode only.
                        maxLength: 2000
                      show_branding:
                        type: boolean
                        description: Workspace-level branding flag. Session mode only.
                      moderate_responses:
                        type: boolean
                        description: Workspace-level default for moderating open-ended responses on ingest. Session mode only.
                  workspaces:
                    type: array
                    description: All workspaces the user belongs to in the current company, sorted by name. Session mode only - powers the dashboard workspace switcher.
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          format: uuid
                          maxLength: 36
                        name:
                          type: string
                          maxLength: 2000
                    maxItems: 500
                  company_id:
                    type: string
                    format: uuid
                    nullable: true
                    description: Company owning the workspace. Null when workspace has no company or in anonymous mode.
                    maxLength: 36
                  plan:
                    type: object
                    properties:
                      tier:
                        type: integer
                        format: int32
                        minimum: 0
                        maximum: 2147483647
                      name:
                        type: string
                        maxLength: 2000
                      node_limit:
                        type: integer
                        nullable: true
                        format: int32
                        minimum: 0
                        maximum: 2147483647
                      show_branding:
                        type: boolean
                      custom_theme:
                        type: boolean
                      response_limit:
                        type: integer
                        nullable: true
                        format: int32
                        minimum: 0
                        maximum: 2147483647

  /connect/me:
    get:
      operationId: getConnectedWorkspace
      tags:
        - Integrations
      summary: Get the connected workspace
      description: Returns the workspace the calling OAuth token or API key is bound to, for use as a connection test.
      security:
        - oauth2: []
        - bearerAuth: []
      responses:
        "200":
          description: The workspace this credential is connected to.
          content:
            application/json:
              schema:
                type: object
                properties:
                  workspace_id:
                    type: string
                    format: uuid
                    example: 550e8400-e29b-41d4-a716-446655440000
                    maxLength: 36
                  workspace_name:
                    type: string
                    example: Acme Marketing
                    maxLength: 2000
        "401":
          description: Missing, invalid, or expired credential.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /connect/forms:
    get:
      operationId: listConnectForms
      tags:
        - Integrations
      summary: List published forms
      description: Lists the workspace's published forms as id, title and share id, for populating a form picker in an integration.
      security:
        - oauth2:
            - forms:read
        - bearerAuth: []
      responses:
        "200":
          description: Published forms in the connected workspace, most recently updated first.
          content:
            application/json:
              schema:
                type: array
                maxItems: 1000
                items:
                  type: object
                  properties:
                    id:
                      type: string
                      format: uuid
                      example: f47ac10b-58cc-4372-a567-0e02b2c3d479
                      maxLength: 36
                    title:
                      type: string
                      example: Customer Feedback
                      maxLength: 2000
                    share_id:
                      type: string
                      example: abc12345
                      maxLength: 2000
        "403":
          description: The credential is missing the forms:read scope.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /connect/hooks:
    post:
      operationId: subscribeWebhook
      tags:
        - Integrations
      summary: Subscribe to form completions
      description: Registers a target URL that receives the form.completed payload for every form in the connected workspace.
      security:
        - oauth2:
            - webhooks:manage
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - targetUrl
              properties:
                targetUrl:
                  type: string
                  format: uri
                  description: HTTPS endpoint Clipform POSTs each completion to. Must be publicly resolvable.
                  example: https://hook.eu1.make.com/abc123
                  maxLength: 2000
      responses:
        "201":
          description: Subscription created, or an existing subscription for the same URL reactivated.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                    description: Subscription id - pass it to the unsubscribe operation.
                    example: 6f4b2c1e-0d3a-4a2b-9c8d-1e2f3a4b5c6d
                    maxLength: 36
        "400":
          description: targetUrl is missing or is not a safe public URL.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: The credential is missing the webhooks:manage scope.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /connect/hooks/{hookId}:
    delete:
      operationId: unsubscribeWebhook
      tags:
        - Integrations
      summary: Unsubscribe from form completions
      description: Deactivates a webhook subscription so Clipform stops POSTing completions to its target URL.
      security:
        - oauth2:
            - webhooks:manage
        - bearerAuth: []
      parameters:
        - name: hookId
          in: path
          required: true
          description: Subscription id returned when the webhook was registered.
          schema:
            type: string
            format: uuid
            example: 6f4b2c1e-0d3a-4a2b-9c8d-1e2f3a4b5c6d
            maxLength: 36
      responses:
        "200":
          description: Subscription deactivated.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
        "400":
          description: The subscription id is not a valid identifier.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: The subscription belongs to a different workspace.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: No subscription with that id.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /forms:
    get:
      operationId: listForms
      tags:
        - Forms
      summary: List forms
      description: Lists the forms in a workspace.
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
            format: int32
          description: Number of forms per page
        - name: cursor
          in: query
          schema:
            type: string
            maxLength: 2000
          description: Opaque pagination cursor from a previous response's `next_cursor`
        - name: tag
          in: query
          schema:
            type: string
            maxLength: 2000
          description: Filter by tag name(s), comma-separated. AND logic - only forms with ALL specified tags are returned.
        - name: published
          in: query
          schema:
            type: string
            enum: ["true", "false"]
          description: Filter by live/draft status
        - name: deleted
          in: query
          schema:
            type: string
            enum: ["true", "false"]
          description: Set to true to list only trashed forms instead of active ones.
        - name: search
          in: query
          schema:
            type: string
            maxLength: 2000
          description: Case-insensitive substring search on form title
        - name: sort
          in: query
          schema:
            type: string
            enum: [created_at, updated_at]
            default: created_at
          description: Sort field
        - name: order
          in: query
          schema:
            type: string
            enum: [asc, desc]
            default: desc
          description: Sort order
        - name: include
          in: query
          schema:
            type: string
            maxLength: 2000
          description: Comma-separated list of related resources to embed. Supported values - `tags`.
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Paginated form list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FormList"
        "401":
          description: Authentication required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          $ref: "#/components/responses/RateLimited"

    post:
      operationId: createForm
      tags:
        - Forms
      summary: Create a form
      description: Creates a form, optionally with all its nodes in one request.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title, workspace_id]
              properties:
                title:
                  type: string
                  maxLength: 200
                  description: Form title
                workspace_id:
                  type: string
                  format: uuid
                  description: Target workspace for the new form. Obtain from GET /v1/me.
                  maxLength: 36
                is_live:
                  type: boolean
                  default: true
                  description: >-
                    Whether the form is immediately live (online for
                    respondents). Defaults to true so created forms are
                    shareable right away - pass false explicitly to create
                    offline.
                first_run:
                  type: boolean
                  default: false
                  description: >-
                    Internal first-run flag. Set by the dashboard cold-start gate
                    when it auto-scaffolds a new user's starter form, to suppress
                    the first_form_created activation milestone - the scaffold is
                    not authoring. Leave unset for normal creates.
                nodes:
                  type: array
                  minItems: 1
                  description: >-
                    Optional batch build: ordered list of nodes to create with
                    the form, all-or-nothing. Each node matches the add-node
                    shape (type, prompt, label?, required?, config?, options?).
                  items:
                    type: object
                    required: [type, prompt]
                    properties:
                      type:
                        type: string
                        description: Node type (see the node-type reference)
                        maxLength: 2000
                      prompt:
                        type: string
                        maxLength: 2000
                      label:
                        type: string
                        maxLength: 2000
                      required:
                        type: boolean
                        default: true
                      config:
                        type: object
                        description: Type-specific configuration
                      options:
                        type: array
                        items:
                          type: object
                          required: [content]
                          properties:
                            content:
                              type: string
                              maxLength: 2000
                            is_correct:
                              type: boolean
                            score:
                              type: number
                            scores:
                              type: object
                              additionalProperties:
                                type: number
                        maxItems: 500
                  maxItems: 500
                settings:
                  type: object
                  description: >-
                    Form settings applied with a batch build (ignored without
                    nodes). If any node has scored options, show_step_counter
                    and disable_back_navigation default to true unless set
                    explicitly here.
                  properties:
                    show_step_counter:
                      type: boolean
                    disable_back_navigation:
                      type: boolean
                    primary_color:
                      type: string
                      description: 6-digit hex, e.g. "#FF5500"
                      maxLength: 20
                    background_color:
                      type: string
                      description: 6-digit hex
                      maxLength: 20
      responses:
        "201":
          description: Form created
          content:
            application/json:
              schema:
                type: object
                properties:
                  form_id:
                    type: string
                    format: uuid
                    maxLength: 36
                  share_id:
                    type: string
                    maxLength: 64
                  form_url:
                    type: string
                    format: uri
                    nullable: true
                    description: Always null. Was the anonymous-caller save link; kept for contract stability since auth became mandatory (#564).
                    maxLength: 2048
                  viewer_url:
                    type: string
                    format: uri
                    maxLength: 2048
                  nodes:
                    type: array
                    description: Present on batch builds - created nodes in order
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          format: uuid
                          maxLength: 36
                        type:
                          type: string
                          maxLength: 2000
                        prompt:
                          type: string
                          maxLength: 2000
                    maxItems: 500
        "401":
          description: Missing or invalid credentials
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: workspace_id does not match the API key's workspace
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          $ref: "#/components/responses/RateLimited"

  /forms/{formId}:
    get:
      operationId: getForm
      tags:
        - Forms
      summary: Get a form
      description: Returns a form and its nodes.
      parameters:
        - name: formId
          in: path
          required: true
          schema:
            type: string
            format: uuid
            maxLength: 36
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Form details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FormWithNodes"
        "429":
          $ref: "#/components/responses/RateLimited"

    patch:
      operationId: updateForm
      tags:
        - Forms
      summary: Update a form
      description: Updates a form.
      parameters:
        - name: formId
          in: path
          required: true
          schema:
            type: string
            format: uuid
            maxLength: 36
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                  maxLength: 200
                  description: The form's own name (form table).
                meta_title:
                  type: string
                  nullable: true
                  description: SEO/social title for search results and link previews (form_metadata.title, surfaced to the viewer as meta_title). Distinct from the form name `title`.
                  maxLength: 2000
                is_live:
                  type: boolean
                show_step_counter:
                  type: boolean
                  description: Show step counter (e.g. '1/5')
                disable_back_navigation:
                  type: boolean
                  description: Prevent respondents from going back
                total_steps:
                  type: integer
                  nullable: true
                  description: Override total step count (null = auto-calculate)
                  format: int32
                  minimum: 0
                  maximum: 2147483647
                show_branding:
                  type: boolean
                  nullable: true
                  description: Per-form override for "Powered by Clipform" badge. null = inherit from workspace, true = show, false = hide. Free plans always show branding regardless.
                moderate_responses:
                  type: boolean
                  nullable: true
                  description: Per-form override for moderating open-ended answers on ingest. null = inherit from workspace, true = moderate, false = do not moderate.
                primary_color:
                  type: string
                  description: Primary/brand color (hex or CSS color)
                  maxLength: 20
                background_color:
                  type: string
                  description: Background color (hex, rgba, or CSS color)
                  maxLength: 20
                font_family:
                  type: string
                  description: Font family name
                  maxLength: 2000
                brand_name:
                  type: string
                  nullable: true
                  description: Brand name shown alongside the logo in the viewer
                  maxLength: 2000
                logo_url:
                  type: string
                  nullable: true
                  description: URL to a logo image shown in the viewer header
                  maxLength: 2048
                description:
                  type: string
                  nullable: true
                  description: SEO description (meta description, og:description)
                  maxLength: 2000
                author:
                  type: string
                  nullable: true
                  description: Author/brand name shown to respondents
                  maxLength: 2000
                allow_resume:
                  type: boolean
                  description: Let returning respondents resume an unfinished session instead of restarting.
                session_expiry_hours:
                  type: integer
                  minimum: 1
                  maximum: 720
                  description: Hours before a saved resume session expires (clamped 1-720).
                  format: int32
                results_public:
                  type: boolean
                  description: Enable the public results portal. The first enable mints a results_share_id server-side; toggling off and back on keeps the same token.
      responses:
        "200":
          description: Form updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  form_id:
                    type: string
                    format: uuid
                    maxLength: 36
                  results_share_id:
                    type: string
                    nullable: true
                    description: Public results portal token, present only when results_public was just enabled for the first time.
                    maxLength: 64
        "429":
          $ref: "#/components/responses/RateLimited"

    delete:
      operationId: deleteForm
      tags:
        - Forms
      summary: Delete a form
      description: Moves a form to the trash and unpublishes it, recoverable via restore.
      parameters:
        - name: formId
          in: path
          required: true
          schema:
            type: string
            format: uuid
            maxLength: 36
      security:
        - bearerAuth: []
      responses:
        "204":
          description: Form moved to the trash
        "429":
          $ref: "#/components/responses/RateLimited"

  /forms/{formId}/restore:
    post:
      operationId: restoreForm
      tags:
        - Forms
      summary: Restore a form
      description: Restores a trashed form as a draft without republishing it.
      parameters:
        - name: formId
          in: path
          required: true
          schema:
            type: string
            format: uuid
            maxLength: 36
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Form restored
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FormSummary"
        "403":
          description: Restoring would exceed the workspace's plan form limit
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          $ref: "#/components/responses/RateLimited"

  /forms/{formId}/permanent:
    delete:
      operationId: deleteFormPermanently
      tags:
        - Forms
      summary: Permanently delete a form
      description: Permanently deletes a form and its nodes, trashed or not.
      parameters:
        - name: formId
          in: path
          required: true
          schema:
            type: string
            format: uuid
            maxLength: 36
      security:
        - bearerAuth: []
      responses:
        "204":
          description: Form permanently deleted
        "429":
          $ref: "#/components/responses/RateLimited"

  /forms/{formId}/nodes:
    post:
      operationId: addNode
      tags:
        - Nodes
      summary: Add a node
      description: Adds a node to a form.
      parameters:
        - name: formId
          in: path
          required: true
          schema:
            type: string
            format: uuid
            maxLength: 36
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [node]
              properties:
                node:
                  $ref: "#/components/schemas/NodeInput"
                after_node_id:
                  type: string
                  format: uuid
                  description: Insert after this node. Omit to append before end screen. Ignored when auto_arrange is false.
                  maxLength: 36
                auto_arrange:
                  type: boolean
                  default: true
                  description: >-
                    When true (default), the new node is wired into the linear logic
                    chain and the canvas is flagged for auto-layout. Set false for manual
                    builder creates so the node lands orphaned (unconnected) and the
                    caller controls its position.
      responses:
        "201":
          description: Node added
          content:
            application/json:
              schema:
                type: object
                properties:
                  node_id:
                    type: string
                    format: uuid
                    maxLength: 36
                  warnings:
                    type: array
                    description: >-
                      Advisory notices, returned only when present - e.g.
                      inserting a terminal node (end_screen/redirect/link_list/
                      shopping) via after_node_id truncated the flow there,
                      orphaning whatever was previously downstream.
                    items:
                      type: string
                      maxLength: 2000
                    maxItems: 500
        "429":
          $ref: "#/components/responses/RateLimited"

  /forms/{formId}/nodes/{nodeId}:
    patch:
      operationId: updateNode
      tags:
        - Nodes
      summary: Update a node
      description: Updates a node.
      parameters:
        - name: formId
          in: path
          required: true
          schema:
            type: string
            format: uuid
            maxLength: 36
        - name: nodeId
          in: path
          required: true
          schema:
            type: string
            format: uuid
            maxLength: 36
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                prompt:
                  type: string
                  maxLength: 500
                type:
                  $ref: "#/components/schemas/NodeType"
                required:
                  type: boolean
                public_results_hidden:
                  type: boolean
                  description: Hide this node's data from the public results portal without affecting the owner Results tab.
                config:
                  type: object
                  description: >-
                    Type-specific configuration, merged shallowly into the stored config.
                    A null value deletes that key; nested objects are replaced wholesale.
                    When type is changed in the same request, config replaces the stored
                    configuration instead of merging.
                options:
                  type: array
                  maxItems: 100
                  items:
                    type: object
                    required: [content]
                    properties:
                      id:
                        type: string
                        format: uuid
                        description: Existing option id to update in place. Omit to insert a new option.
                        maxLength: 36
                      content:
                        type: string
                        maxLength: 200
                      score:
                        type: number
                      scores:
                        type: object
                        additionalProperties:
                          type: number
                  description: >-
                    Upsert-by-id: the node's options are set to this array. Items with a
                    known id are updated in place (ids preserved), items without one are
                    inserted, and existing options absent from the array are deleted.
                    Order follows array position. Omitting all ids performs a full replace.
      responses:
        "200":
          description: Node updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  node_id:
                    type: string
                    format: uuid
                    maxLength: 36
                  options:
                    type: array
                    description: >-
                      The node's resulting option set (real ids, in order), returned only
                      when the request updated options. Lets callers reconcile optimistic
                      temp ids without a refetch.
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          format: uuid
                          maxLength: 36
                        content:
                          type: string
                          maxLength: 2000
                        order:
                          type: integer
                          format: int32
                          minimum: 0
                          maximum: 2147483647
                        score:
                          type: number
                          nullable: true
                        scores:
                          type: object
                          nullable: true
                          additionalProperties:
                            type: number
                    maxItems: 500
                  warnings:
                    type: array
                    description: >-
                      Advisory notices, returned only when present - e.g. scored
                      options were written to a node whose choice.record_scores
                      is explicitly false.
                    items:
                      type: string
                      maxLength: 2000
                    maxItems: 500
        "429":
          $ref: "#/components/responses/RateLimited"

    delete:
      operationId: deleteNode
      tags:
        - Nodes
      summary: Delete a node
      description: Deletes a node and re-links the ones around it.
      parameters:
        - name: formId
          in: path
          required: true
          schema:
            type: string
            format: uuid
            maxLength: 36
        - name: nodeId
          in: path
          required: true
          schema:
            type: string
            format: uuid
            maxLength: 36
      security:
        - bearerAuth: []
      responses:
        "204":
          description: Node deleted
        "429":
          $ref: "#/components/responses/RateLimited"

  /forms/{formId}/nodes/{nodeId}/media:
    post:
      operationId: uploadNodeMedia
      tags:
        - node-media
      summary: Upload node media
      description: Returns a URL to upload media for a node.
      parameters:
        - name: formId
          in: path
          required: true
          schema:
            type: string
            format: uuid
            maxLength: 36
        - name: nodeId
          in: path
          required: true
          schema:
            type: string
            format: uuid
            maxLength: 36
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [media_type]
              properties:
                media_type:
                  type: string
                  enum: [video, still]
                  description: Type of media - video (plays) or still (static image/color, optional audio)
                media_source:
                  type: string
                  enum: [uploaded, recorded]
                  default: uploaded
                  description: How the media was created
                duration:
                  type: number
                  description: Media length in seconds (optional).
                fit_media:
                  type: boolean
                  description: true = contain (show the whole frame), false/omitted = cover (fill the player, croppable, positionable)
      responses:
        "200":
          description: Upload URL created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MediaUploadResponse"
        "429":
          $ref: "#/components/responses/RateLimited"

    get:
      operationId: getNodeMedia
      tags:
        - node-media
      summary: Get node media
      description: Returns the media attached to a node.
      parameters:
        - name: formId
          in: path
          required: true
          schema:
            type: string
            format: uuid
            maxLength: 36
        - name: nodeId
          in: path
          required: true
          schema:
            type: string
            format: uuid
            maxLength: 36
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Media details (or null if no media)
          content:
            application/json:
              schema:
                type: object
                properties:
                  media:
                    $ref: "#/components/schemas/NodeMedia"
        "429":
          $ref: "#/components/responses/RateLimited"

    patch:
      operationId: updateNodeMedia
      tags:
        - node-media
      summary: Update node media
      description: Updates the display properties on a node's media.
      parameters:
        - name: formId
          in: path
          required: true
          schema:
            type: string
            format: uuid
            maxLength: 36
        - name: nodeId
          in: path
          required: true
          schema:
            type: string
            format: uuid
            maxLength: 36
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                position_x:
                  type: number
                fit_media:
                  type: boolean
                show_captions:
                  type: boolean
                options_reveal_seconds:
                  type: number
                  nullable: true
                  description: Seconds to wait after the video starts before answer options become interactable, where null uses the viewer default and 0 is immediate.
      responses:
        "200":
          description: Media updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  node_id:
                    type: string
                    format: uuid
                    maxLength: 36
        "429":
          $ref: "#/components/responses/RateLimited"

    delete:
      operationId: deleteNodeMedia
      tags:
        - node-media
      summary: Delete node media
      description: Removes a node's media.
      parameters:
        - name: formId
          in: path
          required: true
          schema:
            type: string
            format: uuid
            maxLength: 36
        - name: nodeId
          in: path
          required: true
          schema:
            type: string
            format: uuid
            maxLength: 36
      security:
        - bearerAuth: []
      responses:
        "204":
          description: Media deleted (idempotent - also 204 when the node had no media).
        "429":
          $ref: "#/components/responses/RateLimited"

  /forms/{formId}/nodes/{nodeId}/media/captions:
    patch:
      operationId: updateNodeMediaCaptions
      tags:
        - node-media
      summary: Update node media captions
      description: Replaces a node's caption words and regroups them into segments.
      parameters:
        - name: formId
          in: path
          required: true
          schema:
            type: string
            format: uuid
            maxLength: 36
        - name: nodeId
          in: path
          required: true
          schema:
            type: string
            format: uuid
            maxLength: 36
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [words]
              properties:
                words:
                  type: array
                  minItems: 1
                  maxItems: 2000
                  items:
                    type: object
                    required: [word, start, end]
                    properties:
                      word:
                        type: string
                        maxLength: 2000
                      start:
                        type: number
                        description: Start time in seconds
                      end:
                        type: number
                        description: End time in seconds
                  description: Full edited word-level transcript, in order.
      responses:
        "200":
          description: Captions updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  node_id:
                    type: string
                    format: uuid
                    maxLength: 36
                  captions:
                    type: array
                    maxItems: 2000
                    items:
                      type: object
                      properties:
                        start:
                          type: number
                        end:
                          type: number
                        text:
                          type: string
                          maxLength: 2000
                        words:
                          type: array
                          maxItems: 2000
                          items:
                            type: object
                            properties:
                              word:
                                type: string
                                maxLength: 2000
                              start:
                                type: number
                              end:
                                type: number
        "429":
          $ref: "#/components/responses/RateLimited"

  /forms/{formId}/nodes/{nodeId}/media/transcribe:
    post:
      operationId: retryNodeMediaTranscription
      tags:
        - node-media
      summary: Retry node media transcription
      description: Resets a node's video transcription to pending and re-triggers it from the stored media.
      parameters:
        - name: formId
          in: path
          required: true
          schema:
            type: string
            format: uuid
            maxLength: 36
        - name: nodeId
          in: path
          required: true
          schema:
            type: string
            format: uuid
            maxLength: 36
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Transcription restarted
          content:
            application/json:
              schema:
                type: object
                properties:
                  media_id:
                    type: string
                    format: uuid
                    maxLength: 36
                  transcription_status:
                    type: string
                    enum: [pending]
        "404":
          description: Node has no video media to transcribe.
        "429":
          $ref: "#/components/responses/RateLimited"

  /forms/{formId}/tags:
    put:
      operationId: setFormTags
      tags:
        - Tags
      summary: Set form tags
      description: Replaces a form's tags.
      parameters:
        - name: formId
          in: path
          required: true
          schema:
            type: string
            format: uuid
            maxLength: 36
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [tags]
              properties:
                tags:
                  type: array
                  items:
                    type: string
                    maxLength: 2000
                  description: Tag names (e.g. ["quiz", "trivia", "arsenal"])
                  maxItems: 500
      responses:
        "200":
          description: Tags set
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TagList"
        "429":
          $ref: "#/components/responses/RateLimited"
    get:
      operationId: getFormTags
      tags:
        - Tags
      summary: List form tags
      parameters:
        - name: formId
          in: path
          required: true
          schema:
            type: string
            format: uuid
            maxLength: 36
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Tags
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TagList"
        "429":
          $ref: "#/components/responses/RateLimited"

  /forms/{formId}/tags/{tagId}:
    delete:
      operationId: deleteFormTag
      tags:
        - Tags
      summary: Remove a form tag
      parameters:
        - name: formId
          in: path
          required: true
          schema:
            type: string
            format: uuid
            maxLength: 36
        - name: tagId
          in: path
          required: true
          schema:
            type: string
            format: uuid
            maxLength: 36
      security:
        - bearerAuth: []
      responses:
        "204":
          description: Tag removed
        "429":
          $ref: "#/components/responses/RateLimited"

  /forms/{formId}/links:
    get:
      operationId: getFormLinks
      tags:
        - tracked-links
      summary: List tracked links for a form
      description: Returns a form's tracked share links.
      parameters:
        - name: formId
          in: path
          required: true
          schema:
            type: string
            format: uuid
            maxLength: 36
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Tracked links
          content:
            application/json:
              schema:
                type: object
                properties:
                  links:
                    type: array
                    items:
                      $ref: "#/components/schemas/FormLink"
                    maxItems: 500
        "429":
          $ref: "#/components/responses/RateLimited"
    post:
      operationId: createFormLink
      tags:
        - tracked-links
      summary: Create a tracked link
      description: Creates a tracked (UTM) share link.
      parameters:
        - name: formId
          in: path
          required: true
          schema:
            type: string
            format: uuid
            maxLength: 36
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [label]
              properties:
                label:
                  type: string
                  maxLength: 100
                utm_source:
                  type: string
                  nullable: true
                  maxLength: 2000
                utm_medium:
                  type: string
                  nullable: true
                  maxLength: 2000
                utm_campaign:
                  type: string
                  nullable: true
                  maxLength: 2000
                utm_term:
                  type: string
                  nullable: true
                  maxLength: 2000
                utm_content:
                  type: string
                  nullable: true
                  maxLength: 2000
      responses:
        "201":
          description: Tracked link created
          content:
            application/json:
              schema:
                type: object
                properties:
                  link:
                    $ref: "#/components/schemas/FormLink"
        "409":
          description: A link with that label already exists
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          $ref: "#/components/responses/RateLimited"

  /forms/{formId}/links/{linkId}:
    delete:
      operationId: deleteFormLink
      tags:
        - tracked-links
      summary: Delete a tracked link
      parameters:
        - name: formId
          in: path
          required: true
          schema:
            type: string
            format: uuid
            maxLength: 36
        - name: linkId
          in: path
          required: true
          schema:
            type: string
            format: uuid
            maxLength: 36
      security:
        - bearerAuth: []
      responses:
        "204":
          description: Tracked link deleted
        "429":
          $ref: "#/components/responses/RateLimited"

  /forms/{formId}/analytics:
    get:
      operationId: getFormAnalytics
      tags:
        - Analytics
      summary: Get form analytics
      description: Returns funnel analytics for a form.
      parameters:
        - name: formId
          in: path
          required: true
          schema:
            type: string
            format: uuid
            maxLength: 36
        - name: start_date
          in: query
          schema:
            type: string
            format: date-time
            maxLength: 30
          description: Filter from this date (ISO 8601)
        - name: end_date
          in: query
          schema:
            type: string
            format: date-time
            maxLength: 30
          description: Filter until this date (ISO 8601)
        - name: referrer_url
          in: query
          schema:
            type: string
            maxLength: 2000
          description: Filter by referrer URL
        - name: country_code
          in: query
          schema:
            type: string
            maxLength: 2000
          description: Filter by ISO country code
        - name: browser
          in: query
          schema:
            type: string
            maxLength: 2000
          description: Filter by browser name
        - name: device_type
          in: query
          schema:
            type: string
            maxLength: 2000
          description: Filter by device type (desktop, mobile, tablet)
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Analytics data
          content:
            application/json:
              schema:
                type: object
                properties:
                  total_views:
                    type: integer
                    format: int32
                    minimum: 0
                    maximum: 2147483647
                  total_started:
                    type: integer
                    format: int32
                    minimum: 0
                    maximum: 2147483647
                  total_completed:
                    type: integer
                    format: int32
                    minimum: 0
                    maximum: 2147483647
                  total_unique_visitors:
                    type: integer
                    format: int32
                    minimum: 0
                    maximum: 2147483647
                  views_series:
                    type: array
                    items:
                      type: object
                      properties:
                        date:
                          type: string
                          maxLength: 2000
                        value:
                          type: integer
                          format: int32
                          minimum: 0
                          maximum: 2147483647
                    maxItems: 500
                  started_series:
                    type: array
                    items:
                      type: object
                    maxItems: 500
                  completed_series:
                    type: array
                    items:
                      type: object
                    maxItems: 500
        "429":
          $ref: "#/components/responses/RateLimited"

  /forms/{formId}/submissions:
    get:
      operationId: getFormSubmissions
      tags:
        - Analytics
      summary: List submissions
      description: Returns a form's submissions.
      parameters:
        - name: formId
          in: path
          required: true
          schema:
            type: string
            format: uuid
            maxLength: 36
        - name: submission_id
          in: query
          schema:
            type: string
            format: uuid
            maxLength: 36
          description: Filter to a single submission
      security:
        - bearerAuth: []
      responses:
        "200":
          description: List of submissions
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    response_set_id:
                      type: string
                      format: uuid
                      maxLength: 36
                    status:
                      type: string
                      maxLength: 2000
                    created_at:
                      type: string
                      format: date-time
                      maxLength: 30
                    completed_at:
                      type: string
                      format: date-time
                      nullable: true
                      maxLength: 30
                    total_sessions:
                      type: integer
                      format: int32
                      minimum: 0
                      maximum: 2147483647
                    total_responses:
                      type: integer
                      format: int32
                      minimum: 0
                      maximum: 2147483647
                    contact_email:
                      type: string
                      nullable: true
                      maxLength: 2000
                    contact_first_name:
                      type: string
                      nullable: true
                      maxLength: 2000
                    contact_last_name:
                      type: string
                      nullable: true
                      maxLength: 2000
                    node_responses:
                      type: object
                      nullable: true
                    locked:
                      type: boolean
                      description: |
                        True when this response is over the workspace's plan limit. Locked rows have their answer/contact fields nulled.

                maxItems: 500
        "429":
          $ref: "#/components/responses/RateLimited"
  /forms/{formId}/submissions/{submissionId}:
    get:
      operationId: getSubmissionDetails
      tags:
        - Analytics
      summary: Get a submission
      description: Returns one submission in full.
      parameters:
        - name: formId
          in: path
          required: true
          schema:
            type: string
            format: uuid
            maxLength: 36
        - name: submissionId
          in: path
          required: true
          schema:
            type: string
            format: uuid
            maxLength: 36
          description: The submission's response_set_id
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Submission details
          content:
            application/json:
              schema:
                type: object
        "403":
          description: |
            The response is over the workspace's plan limit and locked. Upgrade to view it.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Submission not found for this form
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          $ref: "#/components/responses/RateLimited"

  /forms/{formId}/theme:
    delete:
      operationId: deleteFormTheme
      tags:
        - Forms
      summary: Reset theme to default
      description: Resets a form's theme to defaults.
      parameters:
        - name: formId
          in: path
          required: true
          schema:
            type: string
            format: uuid
            maxLength: 36
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Theme reset
          content:
            application/json:
              schema:
                type: object
                properties:
                  form_id:
                    type: string
                    format: uuid
                    maxLength: 36
                  theme:
                    nullable: true
        "429":
          $ref: "#/components/responses/RateLimited"

  /forms/{formId}/results:
    get:
      operationId: getFormResults
      tags:
        - Analytics
      summary: Get form results
      description: Returns aggregated results for a form.
      parameters:
        - name: formId
          in: path
          required: true
          schema:
            type: string
            format: uuid
            maxLength: 36
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Aggregated results
          content:
            application/json:
              schema:
                type: object
                properties:
                  total_responses:
                    type: integer
                    format: int32
                    minimum: 0
                    maximum: 2147483647
                  public_results_allowed:
                    type: boolean
                    description: Whether this workspace may enable the public results portal, server-computed from the rollout flag and allowlist.
                  nodes:
                    type: array
                    items:
                      type: object
                      properties:
                        node_id:
                          type: string
                          maxLength: 64
                        node_text:
                          type: string
                          maxLength: 2000
                        node_type:
                          type: string
                          maxLength: 2000
                        total_responses:
                          type: integer
                          format: int32
                          minimum: 0
                          maximum: 2147483647
                        total_views:
                          type: integer
                          format: int32
                          minimum: 0
                          maximum: 2147483647
                        answers:
                          type: array
                          items:
                            type: object
                          maxItems: 500
                    maxItems: 500
                  flow:
                    type: object
                    properties:
                      nodes:
                        type: array
                        items:
                          type: object
                        maxItems: 500
                      links:
                        type: array
                        items:
                          type: object
                        maxItems: 500
        "429":
          $ref: "#/components/responses/RateLimited"

  /forms/{formId}/results-share/rotate:
    post:
      operationId: rotateResultsShare
      tags:
        - Forms
      summary: Rotate the public results link
      description: Mints a fresh public results token for a form, invalidating the previous link.
      parameters:
        - name: formId
          in: path
          required: true
          schema:
            type: string
            format: uuid
            maxLength: 36
      security:
        - bearerAuth: []
      responses:
        "200":
          description: New results share token
          content:
            application/json:
              schema:
                type: object
                properties:
                  form_id:
                    type: string
                    format: uuid
                    maxLength: 36
                  results_share_id:
                    type: string
                    description: 24-character lowercase hex token used in the public results URL.
                    maxLength: 64
        "429":
          $ref: "#/components/responses/RateLimited"

  /results/public/{resultsShareId}:
    get:
      operationId: getPublicResults
      tags:
        - Analytics
      summary: Get public results
      description: Returns a form's aggregated results for anyone holding its public results link.
      security: []
      parameters:
        - name: resultsShareId
          in: path
          required: true
          schema:
            type: string
            description: 24-character lowercase hex public results token.
            maxLength: 2000
      responses:
        "200":
          description: Aggregated results, contact/file/payment nodes and locked responses excluded
          content:
            application/json:
              schema:
                type: object
                properties:
                  total_responses:
                    type: integer
                    format: int32
                    minimum: 0
                    maximum: 2147483647
                  nodes:
                    type: array
                    items:
                      type: object
                      properties:
                        node_id:
                          type: string
                          maxLength: 64
                        node_text:
                          type: string
                          maxLength: 2000
                        node_type:
                          type: string
                          maxLength: 2000
                        total_responses:
                          type: integer
                          format: int32
                          minimum: 0
                          maximum: 2147483647
                        total_views:
                          type: integer
                          format: int32
                          minimum: 0
                          maximum: 2147483647
                        answers:
                          type: array
                          items:
                            type: object
                          maxItems: 500
                    maxItems: 500
                  flow:
                    type: object
        "404":
          description: Results not found (unknown or rotated token, or the owner has not enabled public results)
        "429":
          $ref: "#/components/responses/RateLimited"

webhooks:
  form.completed:
    post:
      operationId: webhookFormCompleted
      summary: Form completed
      tags:
        - Webhooks
      description: Sent to your endpoint when a respondent completes a form.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebhookEvent"
      responses:
        "2XX":
          description: Acknowledged. Any 2xx response stops retries.

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: API key (cf_*) passed as Bearer token
    oauth2:
      type: oauth2
      description: Authorization-code OAuth for installed apps acting on a user's workspace. Access tokens (cf_at_*) are passed as Bearer tokens and authorize the Integrations operations. They are opaque, single-audience, server-side records. Client credentials are issued per app - contact support@clipform.io.
      flows:
        authorizationCode:
          authorizationUrl: https://api.clipform.io/oauth/authorize
          tokenUrl: https://api.clipform.io/oauth/token
          refreshUrl: https://api.clipform.io/oauth/token
          scopes:
            forms:read: Read the workspace's forms.
            responses:read: Read the workspace's form responses.
            webhooks:manage: Subscribe and unsubscribe webhook endpoints.
  responses:
    RateLimited:
      description: Rate limit exceeded.
      headers:
        Access-Control-Allow-Origin:
          description: CORS origin permitted to read the response.
          schema:
            type: string
            maxLength: 2048
            example: "*"
        Retry-After:
          description: Seconds to wait before retrying.
          schema:
            type: integer
            format: int32
            minimum: 0
            maximum: 86400
          example: 42
        RateLimit-Limit:
          description: Request quota for the current window.
          schema:
            type: integer
            format: int32
            minimum: 0
            maximum: 1000000
          example: 60
        RateLimit-Remaining:
          description: Requests remaining in the current window.
          schema:
            type: integer
            format: int32
            minimum: 0
            maximum: 1000000
          example: 0
        RateLimit-Reset:
          description: Seconds until the current window resets.
          schema:
            type: integer
            format: int32
            minimum: 0
            maximum: 86400
          example: 42
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
  schemas:
    # === BEGIN GENERATED webhook schemas (pnpm gen:integration-schemas) - DO NOT EDIT ===
    WebhookEvent:
      type: object
      description: Outbound webhook payload. Standard-Webhooks-shaped envelope - the structure is stable and evolves additively within a `version`.
      required:
        - type
        - version
        - timestamp
        - data
      properties:
        type:
          type: string
          maxLength: 1000
          example: form.completed
        version:
          type: string
          maxLength: 1000
          example: "1"
        timestamp:
          type: string
          format: date-time
          maxLength: 30
          example: 2024-01-15T10:30:00Z
        data:
          type: object
          required:
            - form_id
            - form_name
            - share_id
            - form_tags
            - response_id
            - submitted_at
            - session
            - responses
          properties:
            form_id:
              type: string
              format: uuid
              maxLength: 36
              example: f47ac10b-58cc-4372-a567-0e02b2c3d479
            form_name:
              type: string
              maxLength: 1000
              example: Customer Feedback
            share_id:
              type: string
              maxLength: 1000
              example: abc12345
            form_tags:
              type: array
              items:
                type: string
                maxLength: 1000
              maxItems: 20
            response_id:
              type: string
              format: uuid
              maxLength: 36
              example: 550e8400-e29b-41d4-a716-446655440000
            submitted_at:
              type: string
              format: date-time
              maxLength: 30
              example: 2024-01-15T10:30:00Z
            session:
              type: object
              properties:
                device:
                  type: string
                  maxLength: 1000
                  example: desktop
                browser:
                  type: string
                  maxLength: 1000
                  example: Chrome
                country:
                  type: string
                  maxLength: 1000
                  example: United States
                city:
                  type: string
                  maxLength: 1000
                  example: San Francisco
                time_spent_seconds:
                  type: integer
                  format: int32
                  minimum: 0
                  maximum: 2147483647
                  nullable: true
                  example: 45
            responses:
              type: array
              description: The submission's answers, one item per answered node.
              maxItems: 500
              items:
                oneOf:
                  - $ref: "#/components/schemas/WebhookResponseChoice"
                  - $ref: "#/components/schemas/WebhookResponseOpen"
                  - $ref: "#/components/schemas/WebhookResponseDetails"
                  - $ref: "#/components/schemas/WebhookResponseDraw"
                  - $ref: "#/components/schemas/WebhookResponseFile_upload"
                  - $ref: "#/components/schemas/WebhookResponseFile_download"
                  - $ref: "#/components/schemas/WebhookResponseCamera"
                discriminator:
                  propertyName: node_type
                  mapping:
                    choice: "#/components/schemas/WebhookResponseChoice"
                    button: "#/components/schemas/WebhookResponseChoice"
                    open: "#/components/schemas/WebhookResponseOpen"
                    details: "#/components/schemas/WebhookResponseDetails"
                    draw: "#/components/schemas/WebhookResponseDraw"
                    file_upload: "#/components/schemas/WebhookResponseFile_upload"
                    file_download: "#/components/schemas/WebhookResponseFile_download"
                    camera: "#/components/schemas/WebhookResponseCamera"
      example:
        type: form.completed
        version: "1"
        timestamp: 2024-01-15T10:30:00Z
        data:
          form_id: f47ac10b-58cc-4372-a567-0e02b2c3d479
          form_name: Customer Feedback
          share_id: abc12345
          form_tags:
            - sales
            - vip
          response_id: 550e8400-e29b-41d4-a716-446655440000
          submitted_at: 2024-01-15T10:30:00Z
          session:
            device: desktop
            browser: Chrome
            country: United States
            city: San Francisco
            time_spent_seconds: 45
          responses:
            - node_id: 3fa85f64-5717-4562-b3fc-2c963f66afa6
              node_type: choice
              prompt: How did you hear about us?
              label: Option A
              option_id: opt-789
            - node_id: 3fa85f64-5717-4562-b3fc-2c963f66afa6
              node_type: open
              prompt: How did you hear about us?
              text: I loved it
            - node_id: 3fa85f64-5717-4562-b3fc-2c963f66afa6
              node_type: details
              prompt: How did you hear about us?
              fields:
                - type: email
                  value: jane@example.com
                - type: name
                  value: Jane Doe
              consent:
                - name: terms
                  accepted: true
                  type: consent
                  label: I agree to the terms of service and privacy policy.
                - name: marketing
                  accepted: false
                  type: opt_in
                  label: Send me product updates and marketing emails.
            - node_id: 3fa85f64-5717-4562-b3fc-2c963f66afa6
              node_type: draw
              prompt: How did you hear about us?
              media:
                type: image
                content_type: image/png
                filename: drawing.png
                url: https://project-ref.supabase.co/storage/v1/object/sign/media-responses/path/to/file?token=example-token
                download_url: https://project-ref.supabase.co/storage/v1/object/sign/media-responses/path/to/file?token=example-token&download=drawing.png
                expires_at: 2024-01-15T12:30:00Z
            - node_id: 3fa85f64-5717-4562-b3fc-2c963f66afa6
              node_type: file_upload
              prompt: How did you hear about us?
              files:
                - id: 6f4b2c1e-0d3a-4a2b-9c8d-1e2f3a4b5c6d
                  filename: report.pdf
                  content_type: application/pdf
                  size: 102400
                  uploaded_at: 2024-01-15T10:29:00Z
                  url: https://project-ref.supabase.co/storage/v1/object/sign/media-responses/path/to/file?token=example-token
                  download_url: https://project-ref.supabase.co/storage/v1/object/sign/media-responses/path/to/file?token=example-token&download=report.pdf
                  expires_at: 2024-01-15T12:30:00Z
            - node_id: 3fa85f64-5717-4562-b3fc-2c963f66afa6
              node_type: file_download
              prompt: How did you hear about us?
              files:
                - filename: brochure.pdf
                  content_type: application/pdf
                  downloaded_at: 2024-01-15T10:29:00Z
                  url: https://project-ref.supabase.co/storage/v1/object/sign/file-downloads/path/to/file?token=example-token
                  download_url: https://project-ref.supabase.co/storage/v1/object/sign/file-downloads/path/to/file?token=example-token&download=brochure.pdf
                  expires_at: 2024-01-15T12:30:00Z
            - node_id: 3fa85f64-5717-4562-b3fc-2c963f66afa6
              node_type: camera
              prompt: How did you hear about us?
              files:
                - id: 6f4b2c1e-0d3a-4a2b-9c8d-1e2f3a4b5c6d
                  filename: photo.jpg
                  content_type: image/jpeg
                  size: 512000
                  uploaded_at: 2024-01-15T10:29:00Z
                  url: https://project-ref.supabase.co/storage/v1/object/sign/file-uploads/path/to/file?token=example-token
                  download_url: https://project-ref.supabase.co/storage/v1/object/sign/file-uploads/path/to/file?token=example-token&download=photo.jpg
                  expires_at: 2024-01-15T12:30:00Z
    WebhookResponseBase:
      type: object
      required:
        - node_id
        - prompt
        - node_type
      properties:
        node_id:
          type: string
          format: uuid
          maxLength: 36
          example: 3fa85f64-5717-4562-b3fc-2c963f66afa6
        prompt:
          type: string
          maxLength: 1000
          example: How did you hear about us?
        node_type:
          type: string
          maxLength: 1000
          example: choice
          description: The kind of node this answer came from (`choice`, `button`, `open`, `details`, ...). Discriminates the value fields below. Unknown or future types always include `node_id`, `prompt`, and `node_type`, so they can be handled generically.
    WebhookResponseChoice:
      title: Choice or button answer
      allOf:
        - $ref: "#/components/schemas/WebhookResponseBase"
        - type: object
          properties:
            node_type:
              type: string
              enum:
                - choice
                - button
            label:
              type: string
              maxLength: 1000
              example: Option A
              description: The chosen option's text.
            option_id:
              type: string
              maxLength: 1000
              example: opt-789
              description: The chosen option's id.
    WebhookResponseOpen:
      title: Open-ended answer
      allOf:
        - $ref: "#/components/schemas/WebhookResponseBase"
        - type: object
          properties:
            node_type:
              type: string
              enum:
                - open
            text:
              type: string
              maxLength: 1000
              example: I loved it
              description: The respondent's typed answer.
            media:
              type: object
              properties:
                type:
                  type: string
                  maxLength: 1000
                  example: video
                  description: "`audio` or `video`."
                content_type:
                  type: string
                  maxLength: 1000
                  example: video/mp4
                  description: MIME type of the recording.
                filename:
                  type: string
                  maxLength: 1000
                  example: answer.mp4
                  description: Suggested download filename.
                url:
                  type: string
                  format: uri
                  maxLength: 2048
                  example: https://project-ref.supabase.co/storage/v1/object/sign/media-responses/path/to/file?token=example-token
                  description: Signed link to fetch or stream the file inline.
                download_url:
                  type: string
                  format: uri
                  maxLength: 2048
                  example: https://project-ref.supabase.co/storage/v1/object/sign/media-responses/path/to/file?token=example-token&download=answer.mp4
                  description: Same link forced to download.
                expires_at:
                  type: string
                  format: date-time
                  maxLength: 30
                  example: 2024-01-15T12:30:00Z
                  description: When the links stop working.
              description: "Present for audio/video answers. `url` is a direct, time-limited signed link to the recording - valid for about 48 hours and supporting HTTP range requests, so it can be fetched or streamed straight into another tool - and `download_url` is the same link forced to download (Content-Disposition: attachment). After they expire, re-fetch the submission from the authenticated API (using the stable `response_id`) for fresh links."
            transcription:
              type: object
              properties:
                text:
                  type: string
                  maxLength: 1000
                  example: I loved it
                  description: The full transcript.
                language:
                  type: string
                  maxLength: 1000
                  example: en
                  description: BCP-47 language code, when detected.
                segments:
                  type: array
                  items:
                    type: object
                    properties:
                      start:
                        type: number
                        nullable: true
                      end:
                        type: number
                        nullable: true
                      text:
                        type: string
                        maxLength: 1000
                  maxItems: 2000
                  description: Timestamped transcript segments (start/end in seconds) for captions or seeking. Present when word-level timing is available.
              description: Present when the audio/video answer was transcribed.
    WebhookResponseDetails:
      title: Details answer
      allOf:
        - $ref: "#/components/schemas/WebhookResponseBase"
        - type: object
          properties:
            node_type:
              type: string
              enum:
                - details
            fields:
              type: array
              items:
                type: object
                properties:
                  type:
                    type: string
                    maxLength: 1000
                  value:
                    type: string
                    maxLength: 1000
              maxItems: 50
              description: The contact details the respondent filled in. Each entry's `type` is the field kind (e.g. `email`, `first_name`), not the node type. Only filled fields appear.
            consent:
              type: array
              items:
                type: object
                properties:
                  name:
                    type: string
                    maxLength: 1000
                    description: The consent item's field key.
                  accepted:
                    type: boolean
                    description: Whether the respondent accepted this agreement.
                  type:
                    type: string
                    maxLength: 1000
                    description: "`consent` (required) or `opt_in` (optional)."
                  label:
                    type: string
                    maxLength: 1000
                    description: The exact agreement wording, snapshotted at submission.
              maxItems: 200
              description: Consent and opt-in agreements the respondent was shown, present only when the node has them. `accepted` records their choice and `label` is the exact wording they agreed to, snapshotted at submission - editing the agreement text later does not rewrite past responses.
    WebhookResponseDraw:
      title: Drawing answer
      allOf:
        - $ref: "#/components/schemas/WebhookResponseBase"
        - type: object
          properties:
            node_type:
              type: string
              enum:
                - draw
            media:
              type: object
              properties:
                type:
                  type: string
                  maxLength: 1000
                  example: image
                  description: Always `image`.
                content_type:
                  type: string
                  maxLength: 1000
                  example: image/png
                  description: Image MIME type.
                filename:
                  type: string
                  maxLength: 1000
                  example: drawing.png
                  description: Suggested download filename.
                url:
                  type: string
                  format: uri
                  maxLength: 2048
                  example: https://project-ref.supabase.co/storage/v1/object/sign/media-responses/path/to/file?token=example-token
                  description: Signed link to fetch the image inline.
                download_url:
                  type: string
                  format: uri
                  maxLength: 2048
                  example: https://project-ref.supabase.co/storage/v1/object/sign/media-responses/path/to/file?token=example-token&download=drawing.png
                  description: Same link forced to download.
                expires_at:
                  type: string
                  format: date-time
                  maxLength: 30
                  example: 2024-01-15T12:30:00Z
                  description: When the links stop working.
              description: The respondent's drawing, exported as a PNG image. `url` is a direct, time-limited signed link (~48h, HTTP range) and `download_url` is the same link forced to download. Re-fetch from the authenticated API after they expire.
            game:
              type: object
              properties:
                score:
                  type: integer
                  format: int32
                  minimum: 0
                  maximum: 2147483647
                  nullable: true
                  example: 82
                  description: 0-100 match score against the target outline.
                avg_deviation:
                  type: number
                  nullable: true
                  example: 0.08
                  description: Average normalized deviation from the target outline (lower is better).
                tier:
                  type: string
                  maxLength: 1000
                  example: Great match
                  description: The configured tier label the score falls into.
              description: Present only when the node has a configured target shape - the respondent's score against it.
    WebhookResponseFile_upload:
      title: File upload answer
      allOf:
        - $ref: "#/components/schemas/WebhookResponseBase"
        - type: object
          properties:
            node_type:
              type: string
              enum:
                - file_upload
            files:
              type: array
              items:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                    maxLength: 36
                  filename:
                    type: string
                    maxLength: 1000
                  content_type:
                    type: string
                    maxLength: 1000
                  size:
                    type: integer
                    format: int32
                    minimum: 0
                    maximum: 2147483647
                    nullable: true
                  uploaded_at:
                    type: string
                    format: date-time
                    maxLength: 30
                  url:
                    type: string
                    format: uri
                    maxLength: 2048
                  download_url:
                    type: string
                    format: uri
                    maxLength: 2048
                  expires_at:
                    type: string
                    format: date-time
                    maxLength: 30
              maxItems: 20
              description: The uploaded files. Each has a direct, time-limited signed `url` (~48h, HTTP range) and a `download_url` (same link forced to download). Re-fetch from the authenticated API after they expire.
    WebhookResponseFile_download:
      title: File download answer
      allOf:
        - $ref: "#/components/schemas/WebhookResponseBase"
        - type: object
          properties:
            node_type:
              type: string
              enum:
                - file_download
            files:
              type: array
              items:
                type: object
                properties:
                  filename:
                    type: string
                    maxLength: 1000
                  content_type:
                    type: string
                    maxLength: 1000
                  downloaded_at:
                    type: string
                    format: date-time
                    maxLength: 30
                  url:
                    type: string
                    format: uri
                    maxLength: 2048
                  download_url:
                    type: string
                    format: uri
                    maxLength: 2048
                  expires_at:
                    type: string
                    format: date-time
                    maxLength: 30
              maxItems: 20
              description: The files the respondent downloaded. Each has a direct, time-limited signed `url` (~48h, HTTP range) and a `download_url` (same link forced to download). Re-fetch from the authenticated API after they expire.
    WebhookResponseCamera:
      title: Camera answer
      allOf:
        - $ref: "#/components/schemas/WebhookResponseBase"
        - type: object
          properties:
            node_type:
              type: string
              enum:
                - camera
            files:
              type: array
              items:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                    maxLength: 36
                  filename:
                    type: string
                    maxLength: 1000
                  content_type:
                    type: string
                    maxLength: 1000
                  size:
                    type: integer
                    format: int32
                    minimum: 0
                    maximum: 2147483647
                    nullable: true
                  uploaded_at:
                    type: string
                    format: date-time
                    maxLength: 30
                  url:
                    type: string
                    format: uri
                    maxLength: 2048
                  download_url:
                    type: string
                    format: uri
                    maxLength: 2048
                  expires_at:
                    type: string
                    format: date-time
                    maxLength: 30
              maxItems: 20
              description: The captured photo, as a single-item array mirroring file_upload's shape. Has a direct, time-limited signed `url` (~48h, HTTP range) and a `download_url` (same link forced to download). Re-fetch from the authenticated API after they expire.
    # === END GENERATED webhook schemas ===

    Error:
      type: object
      description: Standard error envelope returned by every failed request.
      required: [code, error]
      properties:
        code:
          type: string
          description: Machine-readable error code. Keep in lockstep with the ErrorCode union in apps/api/src/lib/errors.ts (source of truth).
          enum:
            - VALIDATION_ERROR
            - UNKNOWN_SESSION
            - RATE_LIMITED
            - UNAUTHORIZED
            - FORBIDDEN
            - NOT_FOUND
            - INTERNAL_ERROR
            - RENDER_FAILED
            - PAYMENT_ERROR
            - PLAN_LIMIT
            - RESPONSE_LIMIT
            - CONFLICT
        error:
          type: string
          description: Human-readable error message.
          maxLength: 2000
        details:
          description: Optional structured detail; present on some validation errors.

    TagList:
      type: object
      properties:
        tags:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
                maxLength: 36
              name:
                type: string
                maxLength: 2000
              color:
                type: string
                maxLength: 20
          maxItems: 500

    FormLink:
      type: object
      properties:
        id:
          type: string
          format: uuid
          maxLength: 36
        form_id:
          type: string
          format: uuid
          maxLength: 36
        label:
          type: string
          maxLength: 2000
        utm_source:
          type: string
          nullable: true
          maxLength: 2000
        utm_medium:
          type: string
          nullable: true
          maxLength: 2000
        utm_campaign:
          type: string
          nullable: true
          maxLength: 2000
        utm_term:
          type: string
          nullable: true
          maxLength: 2000
        utm_content:
          type: string
          nullable: true
          maxLength: 2000
        created_at:
          type: string
          format: date-time
          maxLength: 30

    NodeType:
      type: string
      enum: [choice, open, button, details, draw, file_download, redirect, end_screen]

    NodeInput:
      type: object
      required: [type, prompt]
      properties:
        id:
          type: string
          format: uuid
          description: >-
            Optional client-generated node id (RF-style stable identity). When
            provided the node is inserted with this id so the builder's optimistic
            node needs no temp-id swap. Omit to let the server generate one.
          maxLength: 36
        type:
          $ref: "#/components/schemas/NodeType"
        prompt:
          type: string
          maxLength: 500
          description: The text shown to the respondent
        label:
          type: string
          description: Internal short label used in exports and reports (defaults to prompt)
          maxLength: 2000
        required:
          type: boolean
          default: true
        config:
          type: object
          description: |
            Type-specific configuration. Key options by type:
            - choice: { choice: { enable_branching, show_answer_feedback }, selection_mode, randomise_options }
            - open: { formats: [{ format: 'text'|'audio'|'video', order }], max_recording_seconds }
            - end_screen: { title, message, show_score, icon, cta_type, cta_text, cta_url, score_ranges, scoring_results }
            - redirect: { url, auto_redirect }
            - link_list: { links: [{ id, url, title, description }], auto_redirect }
        options:
          type: array
          maxItems: 100
          items:
            type: object
            required: [content]
            properties:
              id:
                type: string
                format: uuid
                description: >-
                  Client-generated option id (optional). When provided it's used as the
                  row id so the builder's optimistic option is the persisted row (no
                  temp id swap). Omit to let the server generate one.
                maxLength: 36
              content:
                type: string
                maxLength: 200
              score:
                type: integer
                nullable: true
                description: Points awarded when this option is selected. Use `scores` instead to score across multiple named categories.
                format: int32
                minimum: 0
                maximum: 2147483647
              scores:
                type: object
                additionalProperties:
                  type: integer
                  format: int32
                  minimum: 0
                  maximum: 2147483647
                nullable: true
                description: "Per-category scoring map. Keys are category names, values are points. -1 = knockout."
          description: Answer options for choice/button nodes

    Node:
      type: object
      properties:
        id:
          type: string
          format: uuid
          maxLength: 36
        type:
          $ref: "#/components/schemas/NodeType"
        prompt:
          type: string
          maxLength: 2000
        label:
          type: string
          maxLength: 2000
        required:
          type: boolean
        config:
          type: object
        options:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
                maxLength: 36
              content:
                type: string
                maxLength: 2000
              order:
                type: integer
                format: int32
                minimum: 0
                maximum: 2147483647
              score:
                type: integer
                nullable: true
                description: Single-category point value (simple quiz scoring)
                format: int32
                minimum: 0
                maximum: 2147483647
              scores:
                type: object
                additionalProperties:
                  type: integer
                  format: int32
                  minimum: 0
                  maximum: 2147483647
                nullable: true
                description: Per-category scoring map (multi-outcome quizzes)
          maxItems: 500
        media_id:
          type: string
          format: uuid
          nullable: true
          description: Linked node_media row id (returned in builder views - include=canvas/logic/media)
          maxLength: 36

    MediaUploadResponse:
      type: object
      properties:
        media_id:
          type: string
          format: uuid
          maxLength: 36
        upload_url:
          type: string
          format: uri
          description: The URL to upload the file to.
          maxLength: 2048
        upload_method:
          type: string
          enum: [tus, put]
          description: 'How to send the file: "tus" = resumable upload (the tus.io protocol, used for video), "put" = a single PUT request (used for images).'
        status:
          type: string
          enum: [awaiting_upload]

    NodeMedia:
      type: object
      nullable: true
      properties:
        id:
          type: string
          format: uuid
          maxLength: 36
        media_type:
          type: string
          enum: [video, still]
        media_source:
          type: string
          enum: [uploaded, recorded]
        status:
          type: string
          enum: [ready, processing, errored]
        playback_id:
          type: string
          nullable: true
          description: Playback ID (video only)
          maxLength: 64
        storage_path:
          type: string
          nullable: true
          description: Stored file path (image only)
          maxLength: 2000
        background_color:
          type: string
          nullable: true
          description: CSS color/gradient (still color)
          maxLength: 20
        audio_storage_path:
          type: string
          nullable: true
          description: Optional audio for stills (auto-plays, no controls)
          maxLength: 2000
        duration:
          type: integer
          nullable: true
          description: Video duration in seconds
          format: int32
          minimum: 0
          maximum: 2147483647
        video_width:
          type: integer
          nullable: true
          format: int32
          minimum: 0
          maximum: 2147483647
        video_height:
          type: integer
          nullable: true
          format: int32
          minimum: 0
          maximum: 2147483647
        options_reveal_seconds:
          type: number
          nullable: true
          description: Seconds to wait after video starts before answer options appear (null = viewer default 3s, 0 = immediate)
        transcription_status:
          type: string
          enum: [pending, processing, completed, failed, skipped]
        created_at:
          type: string
          format: date-time
          maxLength: 30

    FormSummary:
      type: object
      properties:
        id:
          type: string
          format: uuid
          maxLength: 36
        title:
          type: string
          maxLength: 2000
        share_id:
          type: string
          maxLength: 64
        is_live:
          type: boolean
        has_unpublished_changes:
          type: boolean
          description: True when the working copy has drifted from the published snapshot.
        created_at:
          type: string
          format: date-time
          maxLength: 30
        updated_at:
          type: string
          format: date-time
          maxLength: 30
        deleted_at:
          type: string
          format: date-time
          nullable: true
          description: When the form was moved to the trash. Null for an active form.
          maxLength: 30
        tags:
          type: array
          description: Only present when `?include=tags` is passed.
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
                maxLength: 36
              name:
                type: string
                maxLength: 2000
              color:
                type: string
                nullable: true
                maxLength: 20
          maxItems: 500

    FormList:
      type: object
      properties:
        forms:
          type: array
          items:
            $ref: "#/components/schemas/FormSummary"
          maxItems: 500
        next_cursor:
          type: string
          nullable: true
          description: Opaque cursor for the next page. Null when there are no more results.
          maxLength: 2000

    ContentMedia:
      type: object
      properties:
        url:
          type: string
          format: uri
          maxLength: 2048
        thumbUrl:
          type: string
          format: uri
          nullable: true
          maxLength: 2048
        width:
          type: integer
          format: int32
          minimum: 0
          maximum: 2147483647
        height:
          type: integer
          format: int32
          minimum: 0
          maximum: 2147483647
        title:
          type: string
          nullable: true
          maxLength: 2000
        attribution:
          type: string
          maxLength: 2000
        license:
          type: string
          maxLength: 2000
        source:
          type: string
          description: Provider name (e.g. Pexels, Unsplash)
          maxLength: 2000
        sourceUrl:
          type: string
          format: uri
          maxLength: 2048

    ContentNewsItem:
      type: object
      properties:
        title:
          type: string
          maxLength: 2000
        description:
          type: string
          nullable: true
          maxLength: 2000
        source:
          type: string
          maxLength: 2000
        author:
          type: string
          nullable: true
          maxLength: 2000
        url:
          type: string
          format: uri
          maxLength: 2048
        imageUrl:
          type: string
          format: uri
          nullable: true
          maxLength: 2048
        publishedAt:
          type: string
          maxLength: 2000

    ContentMusic:
      type: object
      properties:
        id:
          type: string
          maxLength: 64
        name:
          type: string
          maxLength: 2000
        duration:
          type: number
          description: Duration in seconds
        artistName:
          type: string
          maxLength: 2000
        audioUrl:
          type: string
          format: uri
          maxLength: 2048
        downloadUrl:
          type: string
          format: uri
          nullable: true
          maxLength: 2048
        tags:
          type: array
          items:
            type: string
            maxLength: 2000
          maxItems: 500
        instrumental:
          type: boolean
        license:
          type: string
          maxLength: 2000
        source:
          type: string
          maxLength: 2000
        sourceUrl:
          type: string
          format: uri
          maxLength: 2048

    TtsResponse:
      type: object
      properties:
        audio_url:
          type: string
          format: uri
          description: Public URL to the generated audio file
          maxLength: 2048
        storage_path:
          type: string
          description: Stored file path
          maxLength: 2000
        captions:
          type: array
          items:
            type: object
            properties:
              word:
                type: string
                maxLength: 2000
              start:
                type: number
                description: Start time in seconds
              end:
                type: number
                description: End time in seconds
          description: Word-level timing data for caption sync
          maxItems: 500
        voice:
          type: string
          maxLength: 2000

    MontageResponse:
      type: object
      properties:
        storage_path:
          type: string
          description: Stored file path
          maxLength: 2000
        public_url:
          type: string
          format: uri
          nullable: true
          maxLength: 2048
        signed_url:
          type: string
          format: uri
          nullable: true
          maxLength: 2048
        duration_seconds:
          type: number

    RenderResponse:
      type: object
      properties:
        storage_path:
          type: string
          description: Stored file path
          maxLength: 2000
        public_url:
          type: string
          format: uri
          maxLength: 2048
        format:
          type: string
          enum: [mp4, png]
        cache_hit:
          type: boolean
          description: True when the render was served from the render cache (identical composition + props rendered before)

    Composition:
      type: object
      properties:
        id:
          type: string
          description: Composition ID for use with POST /creative/render
          maxLength: 64
        description:
          type: string
          maxLength: 2000
        width:
          type: integer
          format: int32
          minimum: 0
          maximum: 2147483647
        height:
          type: integer
          format: int32
          minimum: 0
          maximum: 2147483647
        fps:
          type: integer
          format: int32
          minimum: 0
          maximum: 2147483647
        durationInFrames:
          type: integer
          format: int32
          minimum: 0
          maximum: 2147483647
        props:
          type: object
          description: Input props schema

    FormWithNodes:
      type: object
      properties:
        form_id:
          type: string
          format: uuid
          maxLength: 36
        title:
          type: string
          maxLength: 2000
        is_live:
          type: boolean
          description: Whether the form is online for respondents (separate from publish state).
        has_unpublished_changes:
          type: boolean
          description: >-
            True when the live working copy has drifted from the published
            snapshot respondents currently see. Cleared by publishing (sending
            `is_live: true`, which re-freezes the snapshot).
        published_version_id:
          type: string
          format: uuid
          nullable: true
          description: The currently published form_version, or null if never published.
          maxLength: 36
        nodes:
          type: array
          items:
            $ref: "#/components/schemas/Node"
          maxItems: 500
