openapi: 3.0.3
info:
  title: Invonio API
  version: "1.0"
  description: >
    REST API for Invonio — compliant EU e-invoicing (ZUGFeRD, Factur-X,
    XRechnung, Peppol). This is the exact same service layer the web app and
    mobile app consume, so behavior is identical across every surface.
  contact:
    name: Invonio
    url: https://invonio.talivio.com
servers:
  - url: https://invonio.talivio.com/api/v1

security:
  - bearerAuth: []

tags:
  - name: Auth
  - name: Account
  - name: Billing
  - name: Business
  - name: Invoices
  - name: Incoming invoices
  - name: Stats & channels

paths:
  /auth/register:
    post:
      tags: [Auth]
      summary: Create an account and issue a device token
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, email, password, password_confirmation, terms, device_name]
              properties:
                name: { type: string }
                email: { type: string, format: email }
                password: { type: string, format: password }
                password_confirmation: { type: string, format: password }
                terms: { type: boolean, description: "Must be true — Privacy Policy & Terms consent" }
                device_name: { type: string, example: "iPhone 16" }
      responses:
        '201':
          description: Account created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AuthResponse' }
        '422': { description: Validation error }

  /auth/login:
    post:
      tags: [Auth]
      summary: Authenticate and issue a device token
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password, device_name]
              properties:
                email: { type: string, format: email }
                password: { type: string, format: password }
                device_name: { type: string }
      responses:
        '200':
          description: Authenticated
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AuthResponse' }
        '422': { description: Invalid credentials }

  /auth/logout:
    post:
      tags: [Auth]
      summary: Revoke the current device token
      responses:
        '204': { description: Token revoked }

  /me:
    get:
      tags: [Account]
      summary: Get the current account
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/User' }
    patch:
      tags: [Account]
      summary: Update name or interface locale
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                locale: { type: string, enum: [en, de, fr] }
      responses:
        '200': { description: Updated }

  /account/export:
    get:
      tags: [Account]
      summary: GDPR data export (Art. 20) — full JSON copy of your data
      responses:
        '200': { description: OK }

  /account:
    delete:
      tags: [Account]
      summary: GDPR erasure (Art. 17) — deletes the account and all data
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [password]
              properties:
                password: { type: string, format: password }
      responses:
        '204': { description: Deleted }
        '422': { description: Wrong password }

  /billing:
    get:
      tags: [Billing]
      summary: Current subscription status and plan pricing
      responses:
        '200': { description: OK }

  /billing/checkout:
    post:
      tags: [Billing]
      summary: Start a Stripe Checkout session for the entry subscription
      responses:
        '200':
          description: Returns a Checkout URL to redirect the user to
        '409': { description: Already subscribed }
        '503': { description: Billing not configured }

  /billing/portal:
    get:
      tags: [Billing]
      summary: Stripe billing portal URL (manage card, cancel)
      responses:
        '200': { description: OK }

  /business:
    get:
      tags: [Business]
      summary: Get the seller profile
      responses:
        '200':
          description: OK (data is null if not yet set up)
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { oneOf: [{ $ref: '#/components/schemas/Business' }, { type: 'null' }] }
    put:
      tags: [Business]
      summary: Create or update the seller profile
      requestBody:
        content:
          application/json:
            schema: { $ref: '#/components/schemas/Business' }
      responses:
        '200': { description: Saved }

  /invoices:
    get:
      tags: [Invoices]
      summary: List invoices (paginated, 50/page)
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/Invoice' } }
        '402': { description: No active subscription }
    post:
      tags: [Invoices]
      summary: Create a draft invoice
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/InvoiceInput' }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/Invoice' }
        '409': { description: No business profile set up yet }
        '422': { description: Validation error }

  /invoices/{invoice}:
    get:
      tags: [Invoices]
      summary: Get an invoice
      parameters: [{ $ref: '#/components/parameters/InvoiceId' }]
      responses:
        '200': { description: OK }
        '403': { description: Not your invoice }
    put:
      tags: [Invoices]
      summary: Update a draft invoice (locked once sent)
      parameters: [{ $ref: '#/components/parameters/InvoiceId' }]
      requestBody:
        content:
          application/json:
            schema: { $ref: '#/components/schemas/InvoiceInput' }
      responses:
        '200': { description: Updated }
        '422': { description: "Sent invoices are locked" }
    delete:
      tags: [Invoices]
      summary: Delete a draft invoice (locked once sent)
      parameters: [{ $ref: '#/components/parameters/InvoiceId' }]
      responses:
        '204': { description: Deleted }
        '422': { description: "Sent invoices are locked" }

  /invoices/{invoice}/document:
    get:
      tags: [Invoices]
      summary: Download the invoice document
      parameters:
        - { $ref: '#/components/parameters/InvoiceId' }
        - name: format
          in: query
          schema: { type: string, enum: [zugferd-en16931, xrechnung-cii, xrechnung-ubl, peppol-bis], default: zugferd-en16931 }
      responses:
        '200':
          description: The document (PDF for zugferd-en16931, XML otherwise)

  /invoices/{invoice}/send:
    post:
      tags: [Invoices]
      summary: Deliver the invoice (email or Peppol) and mark it sent
      parameters: [{ $ref: '#/components/parameters/InvoiceId' }]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                channel: { type: string, default: email, description: "See GET /channels for available keys" }
      responses:
        '200': { description: Sent }
        '422': { description: "e.g. missing buyer email for the email channel" }

  /invoices/{invoice}/duplicate:
    post:
      tags: [Invoices]
      summary: Create a new draft copy (new number, unset sent status)
      parameters: [{ $ref: '#/components/parameters/InvoiceId' }]
      responses:
        '201': { description: Created }

  /invoices/{invoice}/credit-note:
    post:
      tags: [Invoices]
      summary: Create a credit note referencing this (sent) invoice
      parameters: [{ $ref: '#/components/parameters/InvoiceId' }]
      responses:
        '201': { description: Created }
        '422': { description: "Only a sent invoice can be corrected" }

  /invoices/{invoice}/validate:
    post:
      tags: [Invoices]
      summary: Validate the invoice against EN 16931 business rules
      parameters: [{ $ref: '#/components/parameters/InvoiceId' }]
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  valid: { type: boolean }
                  errors: { type: array, items: { type: string } }

  /incoming:
    get:
      tags: [Incoming invoices]
      summary: List received invoices (paginated, 50/page)
      responses:
        '200': { description: OK }
    post:
      tags: [Incoming invoices]
      summary: Upload a received invoice (PDF or XML, max 10MB)
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file: { type: string, format: binary }
      responses:
        '201': { description: Parsed and archived }
        '422': { description: "Not a recognizable e-invoice" }

  /incoming/{incoming}:
    get:
      tags: [Incoming invoices]
      summary: Get a received invoice
      parameters: [{ $ref: '#/components/parameters/IncomingId' }]
      responses:
        '200': { description: OK }

  /incoming/{incoming}/download:
    get:
      tags: [Incoming invoices]
      summary: Download the original file as received
      parameters: [{ $ref: '#/components/parameters/IncomingId' }]
      responses:
        '200': { description: The original PDF or XML }

  /stats:
    get:
      tags: [Stats & channels]
      summary: Dashboard summary — revenue, counts, recent activity
      responses:
        '200': { description: "OK (data is null with no business profile yet)" }

  /channels:
    get:
      tags: [Stats & channels]
      summary: List available send channels (email is always available)
      responses:
        '200': { description: OK }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: Sanctum personal access token from /auth/register or /auth/login
  parameters:
    InvoiceId:
      name: invoice
      in: path
      required: true
      schema: { type: integer }
    IncomingId:
      name: incoming
      in: path
      required: true
      schema: { type: integer }
  schemas:
    User:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        email: { type: string }
        locale: { type: string, nullable: true }
        email_verified: { type: boolean }
        has_business: { type: boolean }
        has_subscription: { type: boolean }
    AuthResponse:
      type: object
      properties:
        token: { type: string, description: "Bearer token — store securely, send as Authorization: Bearer {token}" }
        user: { $ref: '#/components/schemas/User' }
    Business:
      type: object
      required: [name, address_line1, postal_code, city, country_code]
      properties:
        name: { type: string }
        address_line1: { type: string }
        postal_code: { type: string }
        city: { type: string }
        country_code: { type: string, minLength: 2, maxLength: 2, example: "DE" }
        vat_id: { type: string, nullable: true }
        tax_number: { type: string, nullable: true }
        email: { type: string, nullable: true, description: "Required by XRechnung as the seller contact" }
        contact_name: { type: string, nullable: true }
        iban: { type: string, nullable: true }
        bank_account_name: { type: string, nullable: true }
    InvoiceLineInput:
      type: object
      required: [name, quantity, unit_price, vat_rate, unit_code]
      properties:
        name: { type: string }
        description: { type: string, nullable: true }
        quantity: { type: number }
        unit_price: { type: number }
        vat_rate: { type: number, description: "Percent, e.g. 19 for 19%" }
        unit_code: { type: string, example: "HUR", description: "UN/ECE Recommendation 20 unit code" }
    InvoiceInput:
      type: object
      required: [number, issue_date, buyer_name, buyer_address_line1, buyer_postal_code, buyer_city, buyer_country_code, lines]
      properties:
        number: { type: string }
        issue_date: { type: string, format: date }
        due_date: { type: string, format: date, nullable: true }
        buyer_name: { type: string }
        buyer_address_line1: { type: string }
        buyer_postal_code: { type: string }
        buyer_city: { type: string }
        buyer_country_code: { type: string, minLength: 2, maxLength: 2 }
        buyer_email: { type: string, nullable: true }
        buyer_reference: { type: string, nullable: true, description: "Leitweg-ID for German public-sector buyers" }
        buyer_vat_id: { type: string, nullable: true, description: "Required when vat_scheme=reverse_charge" }
        buyer_peppol_scheme: { type: string, nullable: true }
        buyer_peppol_id: { type: string, nullable: true }
        vat_scheme: { type: string, enum: [standard, kleinunternehmer, reverse_charge], default: standard }
        discount_percent: { type: number, nullable: true }
        discount_reason: { type: string, nullable: true }
        payment_terms: { type: string, nullable: true }
        note: { type: string, nullable: true }
        lines: { type: array, items: { $ref: '#/components/schemas/InvoiceLineInput' } }
    Invoice:
      type: object
      properties:
        id: { type: integer }
        number: { type: string }
        type_code: { type: integer, description: "380 = invoice, 381 = credit note" }
        is_credit_note: { type: boolean }
        preceding_invoice_number: { type: string, nullable: true }
        status: { type: string, enum: [draft, sent] }
        issue_date: { type: string, format: date }
        due_date: { type: string, format: date, nullable: true }
        currency: { type: string }
        buyer: { type: object }
        lines: { type: array, items: { type: object } }
        totals:
          type: object
          properties:
            net: { type: number }
            vat: { type: number }
            gross: { type: number }
        sent_at: { type: string, format: date-time, nullable: true }
        sent_to: { type: string, nullable: true }
        documents:
          type: object
          description: "Map of format key -> document download URL"
        created_at: { type: string, format: date-time }
