openapi: 3.0.3
info:
  title: Formvoy API — Zapier Integration
  description: >
    REST endpoints used by the Formvoy Zapier integration. Authenticated with a
    workspace (tenant) API key sent as a bearer token. Covers the REST-hooks
    trigger (subscribe / unsubscribe), the trigger-configuration helpers (form
    list, sample data, recent submissions), and the admin-only API-key
    management endpoints.
  version: "1.0.0"
  contact:
    name: Formvoy
    url: https://app.formvoy.com

servers:
  - url: https://app.formvoy.com
    description: Production
  - url: http://localhost:4200
    description: Local development

security:
  - apiKey: []

tags:
  - name: Zapier
    description: REST-hooks trigger and trigger-configuration endpoints (API-key auth).
  - name: API Key
    description: Workspace API-key management (admin web-app session auth).

paths:
  /api/zapier/sample:
    get:
      tags: [Zapier]
      summary: Sample data / connection auth test
      description: >
        Returns a one-element array containing a sample submission event. Called
        without `formId` it returns a generic placeholder (used as the
        connection auth test — any 2xx confirms the API key). Called with
        `formId` it shapes the `answers` object using that form's real field
        keys with type-appropriate placeholder values.
      operationId: getSample
      parameters:
        - $ref: '#/components/parameters/FormIdQuery'
      responses:
        '200':
          description: Array of one sample event.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/SubmissionEvent'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/zapier/forms:
    get:
      tags: [Zapier]
      summary: List active forms
      description: >
        Powers the "Form" dropdown in the Zap trigger setup. Returns active
        forms belonging to the authenticated workspace only.
      operationId: listForms
      responses:
        '200':
          description: Active forms.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/FormSummary'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'

  /api/zapier/recent:
    get:
      tags: [Zapier]
      summary: Recent submissions for trigger testing
      description: >
        Returns up to 10 of the most recent submissions for a form, shaped
        identically to live webhook deliveries. Without `formId` returns a
        generic placeholder event (Zapier calls this during connection setup
        before a form is picked). With `formId` but no submissions yet, falls
        back to a form-shaped sample so the data picker always has something to
        show.
      operationId: getRecent
      parameters:
        - $ref: '#/components/parameters/FormIdQuery'
      responses:
        '200':
          description: Array of recent submission events (most recent first).
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/SubmissionEvent'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/zapier/subscriptions:
    post:
      tags: [Zapier]
      summary: Subscribe (register REST-hook callback)
      description: >
        Registers a callback URL to receive new submissions for a single form.
        Idempotent on `(form, targetUrl)` — subscribing the same pair again
        returns the existing subscription id and re-enables it rather than
        creating a duplicate.
      operationId: subscribe
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SubscribeRequest'
      responses:
        '200':
          description: Subscription created (or existing one returned).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubscribeResponse'
        '400':
          $ref: '#/components/responses/ValidationFailed'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/zapier/subscriptions/{id}:
    delete:
      tags: [Zapier]
      summary: Unsubscribe (remove REST-hook callback)
      description: >
        Removes a subscription. Idempotent — unsubscribing an already-removed or
        unknown subscription still returns `{ "ok": true }`. Exempt from the
        Premium plan check so Zaps on a downgraded workspace can tear themselves
        down cleanly.
      operationId: unsubscribe
      parameters:
        - name: id
          in: path
          required: true
          description: Subscription id returned from subscribe.
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Unsubscribed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: true
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/me/tenant/api-key:
    get:
      tags: [API Key]
      summary: Read or mint the workspace API key
      description: >
        Admin-only. The first call on a fresh workspace generates the key.
        Requires a Premium plan. Authenticated by the web-app session (JWT), not
        the API key — Zapier itself does not call this.
      operationId: getApiKey
      security:
        - bearerJwt: []
      responses:
        '200':
          description: The workspace API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyView'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'

  /api/me/tenant/api-key/rotate:
    post:
      tags: [API Key]
      summary: Rotate the workspace API key
      description: >
        Admin-only. Replaces the key with a new one. Existing Zaps must
        reconnect with the new key. Authenticated by the web-app session (JWT).
      operationId: rotateApiKey
      security:
        - bearerJwt: []
      responses:
        '200':
          description: The new workspace API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyView'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'

components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: >
        Workspace (tenant) API key sent as `Authorization: Bearer <api-key>`.
        Obtain it as a workspace admin via the API-key management endpoints.
    bearerJwt:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Web-app login session token. Used only for API-key management.

  parameters:
    FormIdQuery:
      name: formId
      in: query
      required: false
      description: >
        Id of a form belonging to the authenticated workspace. When omitted, a
        generic placeholder event is returned.
      schema:
        type: integer
        format: int64
        example: 1234

  responses:
    Unauthorized:
      description: Missing, malformed, or invalid API key.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            timestamp: "2026-06-24T12:34:56Z"
            status: 401
            error: Unauthorized
            message: Invalid API key.
    PaymentRequired:
      description: Workspace is on the Free plan; Zapier requires Premium.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            timestamp: "2026-06-24T12:34:56Z"
            status: 402
            error: Payment Required
            message: Zapier is a Premium feature. Upgrade to connect this workspace.
    NotFound:
      description: Form not found, or not owned by the authenticated workspace.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            timestamp: "2026-06-24T12:34:56Z"
            status: 404
            error: Not Found
            message: form not found
    ValidationFailed:
      description: Request body failed validation.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            timestamp: "2026-06-24T12:34:56Z"
            status: 400
            error: Bad Request
            message: validation failed
            details:
              formId: must not be null

  schemas:
    SubscribeRequest:
      type: object
      required: [formId, targetUrl]
      properties:
        formId:
          type: integer
          format: int64
          description: A form belonging to the authenticated workspace.
          example: 1234
        targetUrl:
          type: string
          maxLength: 2048
          description: Zapier callback URL to POST submissions to.
          example: https://hooks.zapier.com/hooks/standard/abc/def/

    SubscribeResponse:
      type: object
      properties:
        id:
          type: integer
          format: int64
          description: Subscription id. Pass back to unsubscribe.
          example: 5678

    FormSummary:
      type: object
      properties:
        id:
          type: integer
          format: int64
          example: 1234
        name:
          type: string
          example: Lead capture
        kind:
          $ref: '#/components/schemas/FormKind'

    FormKind:
      type: string
      nullable: true
      enum: [PUBLIC, PRIVATE]
      description: >
        PUBLIC = open/anonymous form; PRIVATE = sent to tracked recipients.

    ApiKeyView:
      type: object
      properties:
        apiKey:
          type: string
          example: k_3f9a2b8c7d6e5f40
        createdAt:
          type: string
          format: date-time
          nullable: true
          example: "2026-06-24T12:00:00Z"

    SubmissionEvent:
      type: object
      description: >
        One submission event. The same shape is used for live webhook
        deliveries, /recent, and /sample.
      properties:
        id:
          type: string
          description: Unique event id (the submission id). Zapier dedup key.
          example: "98765"
        event:
          type: string
          description: Always `submission.created`.
          example: submission.created
        form:
          $ref: '#/components/schemas/EventForm'
        submission:
          $ref: '#/components/schemas/EventSubmission'
        recipient:
          $ref: '#/components/schemas/EventRecipient'
        answers:
          type: object
          description: >
            Map of form field key to answer value. Value type depends on the
            field type: string (short/long text, email, single choice, date as
            YYYY-MM-DD), number (number, rating), boolean (yes/no), or array of
            strings (multiple choice). Section headers are omitted.
          additionalProperties: true
          example:
            full_name: Sample answer
            email: alex@example.com
            rating: 4
            subscribe: true

    EventForm:
      type: object
      properties:
        id:
          type: integer
          format: int64
          example: 1234
        name:
          type: string
          example: Lead capture
        kind:
          $ref: '#/components/schemas/FormKind'

    EventSubmission:
      type: object
      properties:
        id:
          type: integer
          format: int64
          example: 98765
        submittedAt:
          type: string
          format: date-time
          nullable: true
          example: "2026-06-24T12:34:56Z"

    EventRecipient:
      type: object
      nullable: true
      description: >
        Present for PRIVATE forms sent to a tracked recipient; null for
        anonymous PUBLIC submissions.
      properties:
        email:
          type: string
          example: alex@example.com
        name:
          type: string
          example: Alex Sample
        company:
          type: string
          example: Acme Co
        phone:
          type: string
          example: "+1 555 0100"

    Error:
      type: object
      properties:
        timestamp:
          type: string
          format: date-time
        status:
          type: integer
        error:
          type: string
        message:
          type: string
        details:
          type: object
          description: Present only on validation failures (field → message).
          additionalProperties:
            type: string
