openapi: 3.1.0
info:
  title: Factura Next Business API
  version: 1.0.0
  description: >-
    Server-to-server API for business data, clients, suppliers, internal invoices,
    expenses, payments, receipts, and return history/creation. DGI issuance,
    PDFs, and webhooks are not available in this release.
servers:
  - url: https://{host}/api/v1
    variables:
      host:
        default: your-factura-next-host
security:
  - BusinessApiKey: []
paths:
  /business:
    get:
      operationId: getBusiness
      summary: Read the business profile
      responses:
        '200': { description: Business profile }
        '401': { $ref: '#/components/responses/Error' }
  /plan/usage:
    get:
      operationId: getPlanUsage
      summary: Read plan and annual invoice usage
      responses:
        '200': { description: 'Plan usage. base_plan retains the stable basic/pro values; base_plan_name is Plan Adaptativo or Plan Illimitado.' }
        '403': { $ref: '#/components/responses/Error' }
  /reference/tax-types:
    get:
      operationId: getTaxTypes
      summary: List supported tax types
      responses:
        '200': { description: Tax types }
  /clients:
    get:
      operationId: listClients
      parameters: [{ $ref: '#/components/parameters/Limit' }, { $ref: '#/components/parameters/Offset' }]
      responses:
        '200': { description: Paginated clients }
    post:
      operationId: createClient
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/Party' } } }
      responses:
        '201': { description: Client created }
        '422': { $ref: '#/components/responses/Error' }
  /clients/{clientId}:
    parameters: [{ $ref: '#/components/parameters/ClientId' }]
    get:
      operationId: getClient
      responses: { '200': { description: Client }, '404': { $ref: '#/components/responses/Error' } }
    patch:
      operationId: updateClient
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/Party' } } }
      responses: { '200': { description: Client updated } }
  /clients/{clientId}/{action}:
    parameters:
      - { $ref: '#/components/parameters/ClientId' }
      - name: action
        in: path
        required: true
        schema: { type: string, enum: [archive, restore] }
    post:
      operationId: changeClientArchiveState
      responses: { '200': { description: Client archive state changed } }
  /suppliers:
    get:
      operationId: listSuppliers
      parameters: [{ $ref: '#/components/parameters/Limit' }, { $ref: '#/components/parameters/Offset' }]
      responses: { '200': { description: Paginated suppliers } }
    post:
      operationId: createSupplier
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/Party' } } }
      responses: { '201': { description: Supplier created } }
  /suppliers/{supplierId}:
    parameters:
      - name: supplierId
        in: path
        required: true
        schema: { type: string, format: uuid }
    get:
      operationId: getSupplier
      responses: { '200': { description: Supplier }, '404': { $ref: '#/components/responses/Error' } }
    patch:
      operationId: updateSupplier
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/Party' } } }
      responses: { '200': { description: Supplier updated } }
  /suppliers/{supplierId}/{action}:
    parameters:
      - name: supplierId
        in: path
        required: true
        schema: { type: string, format: uuid }
      - name: action
        in: path
        required: true
        schema: { type: string, enum: [archive, restore] }
    post:
      operationId: changeSupplierArchiveState
      responses: { '200': { description: Supplier archive state changed } }
  /invoices:
    get:
      operationId: listInvoices
      parameters: [{ $ref: '#/components/parameters/Limit' }, { $ref: '#/components/parameters/Offset' }]
      responses: { '200': { description: Paginated internal invoices } }
    post:
      operationId: createInvoice
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/InvoiceCreate' } } }
      responses:
        '201': { description: Internal invoice created; includes limit_reached when invoice 60 is created, and payment_id when a full Contado receipt was recorded at creation }
        '403': { $ref: '#/components/responses/Error' }
        '409': { $ref: '#/components/responses/Error' }
        '422': { $ref: '#/components/responses/Error' }
  /invoices/{invoiceId}:
    parameters:
      - name: invoiceId
        in: path
        required: true
        schema: { type: string, format: uuid }
    get:
      operationId: getInvoice
      responses: { '200': { description: Invoice with lines }, '404': { $ref: '#/components/responses/Error' } }
  /invoices/{invoiceId}/void:
    parameters:
      - name: invoiceId
        in: path
        required: true
        schema: { type: string, format: uuid }
    post:
      operationId: voidInvoice
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      responses:
        '200': { description: Invoice voided }
        '409': { $ref: '#/components/responses/Error' }
  /invoices/{invoiceId}/returns:
    parameters:
      - name: invoiceId
        in: path
        required: true
        schema: { type: string, format: uuid }
      - { $ref: '#/components/parameters/Limit' }
      - { $ref: '#/components/parameters/Offset' }
    get:
      operationId: listInvoiceReturns
      responses: { '200': { description: Paginated return history } }
    post:
      operationId: createInvoiceReturn
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/ReturnCreate' } } }
      responses:
        '201': { description: Return and refund recorded }
        '409': { $ref: '#/components/responses/Error' }
        '422': { $ref: '#/components/responses/Error' }
  /expenses:
    get:
      operationId: listExpenses
      parameters: [{ $ref: '#/components/parameters/Limit' }, { $ref: '#/components/parameters/Offset' }]
      responses: { '200': { description: Paginated expenses } }
    post:
      operationId: createExpense
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/ExpenseCreate' } } }
      responses:
        '201': { description: Expense created }
        '422': { $ref: '#/components/responses/Error' }
  /expenses/{expenseId}:
    parameters:
      - name: expenseId
        in: path
        required: true
        schema: { type: string, format: uuid }
    get:
      operationId: getExpense
      responses: { '200': { description: Expense with lines }, '404': { $ref: '#/components/responses/Error' } }
  /payments:
    get:
      operationId: listPayments
      parameters: [{ $ref: '#/components/parameters/Limit' }, { $ref: '#/components/parameters/Offset' }]
      responses: { '200': { description: Paginated payment records } }
    post:
      operationId: createPayment
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/PaymentCreate' } } }
      responses:
        '201': { description: Payment recorded }
        '409': { $ref: '#/components/responses/Error' }
  /payments/{paymentId}:
    parameters:
      - name: paymentId
        in: path
        required: true
        schema: { type: string, format: uuid }
    get:
      operationId: getPayment
      responses: { '200': { description: Payment record }, '404': { $ref: '#/components/responses/Error' } }
  /receipts:
    get:
      operationId: listReceipts
      parameters: [{ $ref: '#/components/parameters/Limit' }, { $ref: '#/components/parameters/Offset' }]
      responses: { '200': { description: Paginated receipt/payment records } }
  /receipts/{receiptId}:
    parameters:
      - name: receiptId
        in: path
        required: true
        schema: { type: string, format: uuid }
    get:
      operationId: getReceipt
      responses: { '200': { description: Payment record and related invoice }, '404': { $ref: '#/components/responses/Error' } }
  /returns/{returnId}:
    parameters:
      - name: returnId
        in: path
        required: true
        schema: { type: string, format: uuid }
    get:
      operationId: getReturn
      responses: { '200': { description: Return record with lines }, '404': { $ref: '#/components/responses/Error' } }
components:
  securitySchemes:
    BusinessApiKey:
      type: http
      scheme: bearer
      bearerFormat: fn_test_* or fn_live_*
      description: Owner-generated business key. Keep it on the server.
  parameters:
    Limit:
      name: limit
      in: query
      schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
    Offset:
      name: offset
      in: query
      schema: { type: integer, minimum: 0, maximum: 10000, default: 0 }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema: { type: string, minLength: 8, maxLength: 120 }
    ClientId:
      name: clientId
      in: path
      required: true
      schema: { type: string, format: uuid }
  responses:
    Error:
      description: Stable API error
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
  schemas:
    Error:
      type: object
      properties:
        error:
          type: object
          required: [code, message, request_id]
          properties:
            code: { type: string }
            message: { type: string }
            request_id: { type: string, format: uuid }
            fields: { type: array, items: { type: object } }
    Party:
      type: object
      description: Strict camelCase client/supplier fields; unknown fields are rejected.
      properties:
        documentType: { type: string, enum: [rut, ci, passport, other] }
        taxId: { type: string }
        legalName: { type: string }
        tradeName: { type: string }
        contactName: { type: string }
        email: { type: string }
        phone: { type: string }
        address: { type: string }
        locality: { type: string }
        department: { type: string }
        country: { type: string }
    InvoiceCreate:
      type: object
      additionalProperties: false
      required: [client_id, issue_date, payment_method, currency, items]
      properties:
        client_id: { type: string, format: uuid }
        issue_date: { type: string, format: date }
        payment_method: { type: string, enum: [contado, credito] }
        payment_schedule_frequency: { type: string, enum: [weekly, monthly] }
        payment_schedule_installment_count: { type: integer, minimum: 1, maximum: 60 }
        payment_schedule_start_date: { type: string, format: date }
        paid_in_full_at_creation: { type: boolean, default: false, description: "When true, records a full Contado payment and receipt atomically; currently available only in the isolated DGI Testing deployment." }
        payment_date: { type: string, format: date, description: "Required when paid_in_full_at_creation is true; must be between issue_date and today in Uruguay." }
        collection_method: { type: string, enum: [cash, bank_transfer, card, other], default: cash }
        currency: { type: string, enum: [UYU, USD] }
        exchange_rate_uyu_per_usd: { type: number, exclusiveMinimum: 0 }
        purchase_order_number: { type: string, pattern: '^[0-9]{1,30}$' }
        notes: { type: string, maxLength: 1000 }
        items:
          type: array
          minItems: 1
          maxItems: 10
          items:
            type: object
            additionalProperties: false
            required: [description, quantity, unit_price_minor_units, tax_type]
            properties:
              description: { type: string, minLength: 1, maxLength: 240 }
              quantity: { type: integer, minimum: 1 }
              unit_price_minor_units: { type: integer, minimum: 0 }
              tax_type: { type: string, enum: [basica, minima, exento, exportacion] }
    ExpenseCreate:
      type: object
      additionalProperties: false
      required: [expense_date, supplier_id, currency, payment_method, items]
      properties:
        expense_date: { type: string, format: date }
        supplier_id: { type: string, format: uuid }
        currency: { type: string, enum: [UYU, USD] }
        exchange_rate_uyu_per_usd: { type: number, exclusiveMinimum: 0 }
        payment_method: { type: string, enum: [contado, credito] }
        notes: { type: string, maxLength: 240 }
        items:
          type: array
          minItems: 1
          maxItems: 10
          items:
            type: object
            additionalProperties: false
            required: [category, quantity, unit_price_minor_units, tax_type]
            properties:
              category: { type: string, enum: [advertising, bank_fees, contractors, insurance, meals, office_supplies, professional_fees, rent, software, telephone_internet, travel, vehicle, other] }
              description: { type: string, maxLength: 240 }
              quantity: { type: integer, minimum: 1 }
              unit_price_minor_units: { type: integer, minimum: 0 }
              tax_type: { type: string, enum: [basica, minima, exento, exportacion] }
    ReturnCreate:
      type: object
      additionalProperties: false
      required: [return_date, document_type, return_type, refund_method]
      properties:
        return_date: { type: string, format: date }
        document_type: { type: string, enum: [devolucion_contado, nota_credito] }
        return_type: { type: string, enum: [quantity, amount, custom] }
        exchange_rate_uyu_per_usd: { type: [number, 'null'], exclusiveMinimum: 0 }
        refund_method: { type: string, enum: [unspecified, cash, bank_transfer, card, other] }
        notes: { type: [string, 'null'], maxLength: 1000 }
        return_amount_minor_units: { type: [integer, 'null'], minimum: 1 }
        items:
          type: array
          maxItems: 10
          items:
            type: object
            additionalProperties: false
            required: [invoice_item_id, quantity]
            properties:
              invoice_item_id: { type: string, format: uuid }
              quantity: { type: integer, minimum: 1 }
              amount_minor_units: { type: integer, minimum: 1 }    PaymentCreate:
      type: object
      additionalProperties: false
      required: [invoice_id]
      properties:
        invoice_id: { type: string, format: uuid }
        amount_minor_units:
          type: [integer, 'null']
          minimum: 1
          description: Omit or set null to record the full remaining balance; contado invoices require null.
        payment_date: { type: string, format: date }
        payment_method: { type: string, enum: [unspecified, cash, bank_transfer, card, other] }
        notes: { type: [string, 'null'], maxLength: 1000 }
