openapi: 3.0.3

info:
  title: Finova for Developers API
  version: "1.0.0"
  description: |
    Public REST API for Finova workspaces. Manage **Products**, **Customers**,
    **Leads**, and **Automations** in your Finova company from your own site,
    ERP, or backend.

    All requests are authenticated with a Bearer API key issued from
    Settings → Developers inside Finova. Keys are scoped per resource and
    action.

    Base URL: `https://developers.fi-nova.com/api/v1`

    See the full guides at <https://developers.fi-nova.com/>.
  contact:
    name: Finova
    url: https://developers.fi-nova.com/
  license:
    name: Proprietary
    url: https://fi-nova.com/

servers:
  - url: https://developers.fi-nova.com/api/v1
    description: Production
  - url: http://developers.app.localhost:3000/api/v1
    description: Local development

security:
  - BearerAuth: []

tags:
  - name: Products
    description: Catalog of products and services sold by the company.
  - name: Customers
    description: People or companies that have done business with you.
  - name: Leads
    description: Prospects captured from your funnel, forms, or imports.
  - name: Automations
    description: Trigger-based workflows defined inside Finova.

paths:
  /products:
    get:
      tags: [Products]
      summary: List products
      operationId: listProducts
      security:
        - BearerAuth: [read-products]
      parameters:
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: Paginated list of products.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProductList"
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
    post:
      tags: [Products]
      summary: Create a product
      operationId: createProduct
      security:
        - BearerAuth: [create-products]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [data]
              properties:
                data:
                  $ref: "#/components/schemas/ProductInput"
      responses:
        "201":
          description: Product created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ProductEnvelope" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/Unprocessable" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /products/{id}:
    parameters:
      - $ref: "#/components/parameters/ObjectId"
    get:
      tags: [Products]
      summary: Retrieve a product
      operationId: getProduct
      security:
        - BearerAuth: [read-products]
      responses:
        "200":
          description: Product found.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ProductEnvelope" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Products]
      summary: Update a product
      operationId: updateProduct
      security:
        - BearerAuth: [update-products]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [data]
              properties:
                data:
                  $ref: "#/components/schemas/ProductInput"
      responses:
        "200":
          description: Product updated.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ProductEnvelope" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/Unprocessable" }
    delete:
      tags: [Products]
      summary: Delete a product
      operationId: deleteProduct
      security:
        - BearerAuth: [delete-products]
      responses:
        "204": { description: Deleted. }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /customers:
    get:
      tags: [Customers]
      summary: List customers
      operationId: listCustomers
      security:
        - BearerAuth: [read-customers]
      parameters:
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: Paginated list of customers.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CustomerList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
    post:
      tags: [Customers]
      summary: Create a customer
      operationId: createCustomer
      security:
        - BearerAuth: [create-customers]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [data]
              properties:
                data: { $ref: "#/components/schemas/CustomerInput" }
      responses:
        "201":
          description: Customer created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CustomerEnvelope" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/Unprocessable" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /customers/{id}:
    parameters:
      - $ref: "#/components/parameters/ObjectId"
    get:
      tags: [Customers]
      summary: Retrieve a customer
      operationId: getCustomer
      security:
        - BearerAuth: [read-customers]
      responses:
        "200":
          description: Customer found.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CustomerEnvelope" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Customers]
      summary: Update a customer
      operationId: updateCustomer
      security:
        - BearerAuth: [update-customers]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [data]
              properties:
                data: { $ref: "#/components/schemas/CustomerInput" }
      responses:
        "200":
          description: Customer updated.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CustomerEnvelope" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/Unprocessable" }
    delete:
      tags: [Customers]
      summary: Delete a customer
      operationId: deleteCustomer
      security:
        - BearerAuth: [delete-customers]
      responses:
        "204": { description: Deleted. }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /leads:
    get:
      tags: [Leads]
      summary: List leads
      operationId: listLeads
      security:
        - BearerAuth: [read-leads]
      parameters:
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: Paginated list of leads.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/LeadList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
    post:
      tags: [Leads]
      summary: Create a lead
      operationId: createLead
      security:
        - BearerAuth: [create-leads]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [data]
              properties:
                data: { $ref: "#/components/schemas/LeadInput" }
      responses:
        "201":
          description: Lead created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/LeadEnvelope" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/Unprocessable" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /leads/{id}:
    parameters:
      - $ref: "#/components/parameters/ObjectId"
    get:
      tags: [Leads]
      summary: Retrieve a lead
      operationId: getLead
      security:
        - BearerAuth: [read-leads]
      responses:
        "200":
          description: Lead found.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/LeadEnvelope" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Leads]
      summary: Update a lead
      operationId: updateLead
      security:
        - BearerAuth: [update-leads]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [data]
              properties:
                data: { $ref: "#/components/schemas/LeadInput" }
      responses:
        "200":
          description: Lead updated.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/LeadEnvelope" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/Unprocessable" }
    delete:
      tags: [Leads]
      summary: Delete a lead
      operationId: deleteLead
      security:
        - BearerAuth: [delete-leads]
      responses:
        "204": { description: Deleted. }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /automations:
    get:
      tags: [Automations]
      summary: List automations
      operationId: listAutomations
      security:
        - BearerAuth: [read-automations]
      parameters:
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: Paginated list of automations.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AutomationList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
    post:
      tags: [Automations]
      summary: Create an automation (not yet supported)
      description: |
        Authoring automations via API is not supported in v1 — the
        `trigger_config` and `steps` schemas are still evolving. Create
        automations from the Finova UI; use this API to toggle, rename, or
        delete them.
      operationId: createAutomation
      security:
        - BearerAuth: [create-automations]
      responses:
        "422":
          description: Not yet supported.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /automations/{id}:
    parameters:
      - $ref: "#/components/parameters/ObjectId"
    get:
      tags: [Automations]
      summary: Retrieve an automation
      operationId: getAutomation
      security:
        - BearerAuth: [read-automations]
      responses:
        "200":
          description: Automation found.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AutomationEnvelope" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Automations]
      summary: Update an automation
      description: |
        Phase 1 only allows toggling, renaming, and editing the description.
        `trigger_type`, `trigger_config`, and `steps` cannot be modified via
        API.
      operationId: updateAutomation
      security:
        - BearerAuth: [update-automations]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [data]
              properties:
                data: { $ref: "#/components/schemas/AutomationInput" }
      responses:
        "200":
          description: Automation updated.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AutomationEnvelope" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/Unprocessable" }
    delete:
      tags: [Automations]
      summary: Delete an automation
      operationId: deleteAutomation
      security:
        - BearerAuth: [delete-automations]
      responses:
        "204": { description: Deleted. }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: finova_sk_<secret>
      description: |
        Send your API key as `Authorization: Bearer finova_sk_<secret>`.
        Generate keys from Settings → Developers in the Finova UI.

  parameters:
    Page:
      name: page
      in: query
      required: false
      schema: { type: integer, minimum: 1, default: 1 }
      description: 1-indexed page number.
    Limit:
      name: limit
      in: query
      required: false
      schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
      description: Items per page. Hard cap is 100.
    ObjectId:
      name: id
      in: path
      required: true
      schema: { type: string, pattern: "^[a-f0-9]{24}$" }
      description: Identificador del recurso (string hexadecimal de 24 caracteres).
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema: { type: string, maxLength: 255 }
      description: |
        Optional opaque token. Replays of a POST/PATCH with the same key
        within 24 hours return the original response instead of creating a
        duplicate resource. See `/guides/idempotency`.

  responses:
    Unauthorized:
      description: Missing, invalid, or revoked API key.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: invalid_key, message: Authentication failed. }
    Forbidden:
      description: Key is valid but missing the required scope or origin.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: insufficient_scope, message: "This API key does not have the required scope.", required: read-products }
    NotFound:
      description: Resource does not exist or belongs to another company.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: not_found, message: Resource not found. }
    Unprocessable:
      description: Validation failed or bad input.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: validation_failed, message: "Name can't be blank", errors: { name: ["can't be blank"] } }
    TooManyRequests:
      description: Rate limit exceeded (60/min/IP, 600/min/key).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: rate_limited, message: Too many requests. }

  schemas:
    Meta:
      type: object
      properties:
        page:        { type: integer, example: 1 }
        limit:       { type: integer, example: 25 }
        total:       { type: integer, example: 137 }
        total_pages: { type: integer, example: 6 }

    Error:
      type: object
      required: [error, message]
      properties:
        error:    { type: string, example: validation_failed }
        message:  { type: string, example: "Name can't be blank" }
        errors:
          type: object
          additionalProperties:
            type: array
            items: { type: string }
        required:
          type: string
          description: Present on `insufficient_scope` only.

    Product:
      type: object
      properties:
        id:                { type: string, example: 65a0c0f1234567890abcdef0 }
        name:              { type: string, example: "Servicio de página web" }
        slug:              { type: string, nullable: true, example: servicio-pagina-web }
        sku:               { type: string, nullable: true, example: WEB-001 }
        description:       { type: string, nullable: true }
        product_type:      { type: string, enum: [simple, service, variable, composite] }
        is_active:         { type: boolean }
        tags:              { type: array, items: { type: string } }
        category_name:     { type: string, nullable: true }
        currency:          { type: string, example: mxn }
        price_cents:       { type: integer, example: 1500000 }
        inventory_enabled: { type: boolean }
        inventory_mode:    { type: string, enum: [materials, self, hybrid] }
        taxable:           { type: boolean }
        tax_rate_percent:  { type: number, format: float, example: 16.0 }
        created_at:        { type: string, format: date-time }
        updated_at:        { type: string, format: date-time }
    ProductInput:
      type: object
      properties:
        name:              { type: string }
        slug:              { type: string }
        sku:               { type: string }
        description:       { type: string }
        product_type:      { type: string, enum: [simple, service, variable, composite] }
        is_active:         { type: boolean }
        category_name:     { type: string }
        currency:          { type: string }
        price_cents:       { type: integer }
        inventory_enabled: { type: boolean }
        inventory_mode:    { type: string, enum: [materials, self, hybrid] }
        taxable:           { type: boolean }
        tax_rate_percent:  { type: number, format: float }
        tags:              { type: array, items: { type: string } }
      required: [name]
    ProductEnvelope:
      type: object
      properties:
        data: { $ref: "#/components/schemas/Product" }
    ProductList:
      type: object
      properties:
        data: { type: array, items: { $ref: "#/components/schemas/Product" } }
        meta: { $ref: "#/components/schemas/Meta" }

    Customer:
      type: object
      properties:
        id:         { type: string }
        name:       { type: string }
        short_name: { type: string, nullable: true }
        email:      { type: string, format: email, nullable: true }
        phone:      { type: string, nullable: true }
        birthdate:  { type: string, format: date, nullable: true }
        source:     { type: string, nullable: true }
        notes:      { type: string, nullable: true }
        is_active:  { type: boolean }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    CustomerInput:
      type: object
      properties:
        name:       { type: string }
        short_name: { type: string }
        email:      { type: string, format: email }
        phone:      { type: string }
        birthdate:  { type: string, format: date }
        source:     { type: string }
        notes:      { type: string }
        is_active:  { type: boolean }
      required: [name]
    CustomerEnvelope:
      type: object
      properties:
        data: { $ref: "#/components/schemas/Customer" }
    CustomerList:
      type: object
      properties:
        data: { type: array, items: { $ref: "#/components/schemas/Customer" } }
        meta: { $ref: "#/components/schemas/Meta" }

    Lead:
      type: object
      properties:
        id:                       { type: string }
        name:                     { type: string }
        email:                    { type: string, format: email, nullable: true }
        phone:                    { type: string, nullable: true }
        company_name:             { type: string, nullable: true }
        job_title:                { type: string, nullable: true }
        source:                   { type: string, nullable: true }
        preferred_contact_method: { type: string, nullable: true, example: email }
        status:                   { type: string, example: new }
        notes:                    { type: string, nullable: true }
        metadata:
          type: object
          additionalProperties: { type: string }
          description: Free-form string→string map. Total serialized size capped at 4 KB.
        converted:                { type: boolean, description: True if the lead has a customer_id. }
        created_at:               { type: string, format: date-time }
        updated_at:               { type: string, format: date-time }
    LeadInput:
      type: object
      properties:
        name:                     { type: string }
        email:                    { type: string, format: email }
        phone:                    { type: string }
        company_name:             { type: string }
        job_title:                { type: string }
        source:                   { type: string }
        preferred_contact_method: { type: string }
        status:                   { type: string }
        notes:                    { type: string }
        metadata:
          type: object
          additionalProperties: { type: string }
      required: [name]
    LeadEnvelope:
      type: object
      properties:
        data: { $ref: "#/components/schemas/Lead" }
    LeadList:
      type: object
      properties:
        data: { type: array, items: { $ref: "#/components/schemas/Lead" } }
        meta: { $ref: "#/components/schemas/Meta" }

    Automation:
      type: object
      properties:
        id:           { type: string }
        name:         { type: string }
        description:  { type: string, nullable: true }
        active:       { type: boolean }
        trigger_type: { type: string, example: lead_created }
        steps_count:  { type: integer, example: 3 }
        created_at:   { type: string, format: date-time }
        updated_at:   { type: string, format: date-time }
    AutomationInput:
      type: object
      properties:
        name:        { type: string }
        description: { type: string }
        active:      { type: boolean }
    AutomationEnvelope:
      type: object
      properties:
        data: { $ref: "#/components/schemas/Automation" }
    AutomationList:
      type: object
      properties:
        data: { type: array, items: { $ref: "#/components/schemas/Automation" } }
        meta: { $ref: "#/components/schemas/Meta" }
