openapi: 3.1.0
info:
  title: Beeswax STP API
  summary: Single Touch Payroll (STP) Phase 2 lodgement API for the Australian Taxation Office.
  version: "1.0.0"
  description: |
    The Beeswax STP API lets your application submit PAYEVNT.0004 pay events to
    the ATO on behalf of registered employers. It handles XBRL generation,
    ebMS3/AS4 signing, transport, and status tracking — you just POST a pay
    event and read back the ATO response.

    Authentication is per-user Bearer tokens split into test (`stp_test_...`)
    and live (`stp_live_...`) environments. Test tokens exercise the ATO's
    EVTE sandbox; live tokens lodge to real production.

    All mutating endpoints accept an `Idempotency-Key` header.
  contact:
    name: Beeswax STP support
    url: https://stp.beeswaxapp.com/marketing/support
  license:
    name: Proprietary
servers:
  - url: https://stp.beeswaxapp.com
    description: Production

security:
  - BearerAuth: []

tags:
  - name: Employers
  - name: Employees
  - name: Pay events
  - name: Submissions
  - name: YTD
  - name: Events
  - name: Sandbox

paths:
  /api/v1/employers:
    get:
      tags: [Employers]
      summary: List employers
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: Employer list
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Employer' }
    post:
      tags: [Employers]
      summary: Create employer
      parameters: [ { $ref: '#/components/parameters/IdempotencyKey' } ]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/EmployerInput' }
      responses:
        '201':
          description: Employer created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Employer' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /api/v1/employers/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: integer }
    get:
      tags: [Employers]
      summary: Get employer
      responses:
        '200':
          description: Employer detail
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Employer' }
    patch:
      tags: [Employers]
      summary: Update employer
      parameters: [ { $ref: '#/components/parameters/IdempotencyKey' } ]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/EmployerInput' }
      responses:
        '200':
          description: Employer updated
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Employer' }
    delete:
      tags: [Employers]
      summary: Deactivate employer and scrub employee PII
      description: |
        Marks the employer inactive and clears personally identifying fields
        from its employees (TFN, address, email). ATO-required records
        (pay events, submissions, YTD) are retained for 5 financial years
        per ATO record-keeping obligations.
      responses:
        '200':
          description: Employer deactivated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: deactivated }
                  employer_id: { type: integer }
                  message: { type: string }
                  retention_expires_on: { type: string, format: date }

  /api/v1/pay_events:
    post:
      tags: [Pay events]
      summary: Create a pay event
      parameters: [ { $ref: '#/components/parameters/IdempotencyKey' } ]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PayEventInput' }
      responses:
        '201':
          description: Pay event created (draft)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PayEvent' }

  /api/v1/pay_events/{id}/submit:
    post:
      tags: [Pay events]
      summary: Lodge pay event to the ATO
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '202':
          description: Lodgement queued
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PayEvent' }

  /api/v1/events:
    get:
      tags: [Events]
      summary: List webhook events
      description: |
        Cursor-paginated archive of webhook events delivered to your endpoints.
        Use `starting_after` with the last event's ID to paginate, and
        `event_type` to filter by a single type. Entries are ordered newest-first.
      parameters:
        - name: starting_after
          in: query
          schema: { type: string, example: evt_4f1c2b... }
        - name: event_type
          in: query
          schema: { type: string, example: submission.accepted }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
      responses:
        '200':
          description: Events
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Event' }
                  has_more: { type: boolean }
                  next_cursor: { type: string, nullable: true }

  /api/v1/events/{id}:
    get:
      tags: [Events]
      summary: Get webhook event
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, example: evt_4f1c2b... }
      responses:
        '200':
          description: Event detail with full payload
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EventDetail' }

  /api/v1/events/{id}/resend:
    post:
      tags: [Events]
      summary: Re-deliver a webhook event
      description: |
        Queues a fresh delivery of a past webhook event. The replayed delivery
        gets its own `evt_...` ID and carries a `replayed_from_id` pointing
        back at the original.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '202':
          description: Replay queued
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EventDetail' }

  /api/v1/sandbox/seed:
    post:
      tags: [Sandbox]
      summary: Provision a test employer with sample employees
      description: |
        Test-mode only. Creates a `Sandbox-*` employer plus three employees
        (full-time, part-time, casual) you can use to exercise the pay event
        and lodgement flow end-to-end against the ATO EVTE sandbox. Idempotent
        — calling twice returns the existing sandbox employer.
      parameters: [ { $ref: '#/components/parameters/IdempotencyKey' } ]
      responses:
        '201':
          description: Sandbox employer and employees
        '403':
          description: Live token used against sandbox endpoint

  /api/v1/ytd:
    get:
      tags: [YTD]
      summary: Year-to-date amounts
      parameters:
        - name: employer_id
          in: query
          required: true
          schema: { type: integer }
        - name: financial_year
          in: query
          schema: { type: string, example: "2026-27" }
      responses:
        '200':
          description: YTD breakdown per employee
          content:
            application/json:
              schema: { $ref: '#/components/schemas/YtdResponse' }

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: stp_test_... or stp_live_...
      description: |
        API tokens are created in the Beeswax STP dashboard. Tokens have a
        `test` or `live` environment and are scoped to that environment's
        employers. Tokens may optionally require HMAC request signing
        (X-STP-Signature) and restrict CORS to a whitelist of Origins.

  parameters:
    Page:
      name: page
      in: query
      schema: { type: integer, minimum: 1, default: 1 }
    PerPage:
      name: per_page
      in: query
      schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      description: |
        Opaque client-chosen string. Repeat calls with the same key
        within 24 hours return the original response without re-executing.
      schema: { type: string, maxLength: 255 }

  responses:
    ValidationError:
      description: Request failed validation
      content:
        application/json:
          schema:
            type: object
            properties:
              error: { type: string, example: Validation Failed }
              message:
                type: array
                items: { type: string }

  schemas:
    Employer:
      type: object
      properties:
        id: { type: integer }
        legal_name: { type: string }
        trading_name: { type: string, nullable: true }
        abn: { type: string }
        branch_number: { type: integer }
        environment: { type: string, enum: [evte, production] }
        active: { type: boolean }
        software_id: { type: string, nullable: true }
        software_subscription_id:
          type: string
          nullable: true
          description: |
            Software Subscription ID per the ATO DSP Operational Framework
            (mandatory 30 June 2026). 11-character deterministic ID.
        stp_configured: { type: boolean }
        employee_count: { type: integer }

    EmployerInput:
      type: object
      required: [employer]
      properties:
        employer:
          type: object
          required: [legal_name, abn, branch_number]
          properties:
            legal_name: { type: string }
            trading_name: { type: string }
            abn: { type: string, pattern: '^\d{11}$' }
            branch_number: { type: integer, minimum: 1 }
            bms_id: { type: string }
            software_id: { type: string }
            environment: { type: string, enum: [evte, production] }

    PayEvent:
      type: object
      properties:
        id: { type: integer }
        employer_id: { type: integer }
        pay_period_start: { type: string, format: date }
        pay_period_end: { type: string, format: date }
        payment_date: { type: string, format: date }
        status: { type: string, enum: [draft, ready, submitted, accepted, rejected, error] }
        event_type: { type: string, enum: [submit, update, full_file_replacement] }
        is_finalisation: { type: boolean }
        employee_count: { type: integer }
        total_gross: { type: number }
        total_tax: { type: number }
        financial_year: { type: string, example: "2026-27" }
        message_id: { type: string, nullable: true }

    PayEventInput:
      type: object
      required: [pay_event]
      properties:
        pay_event:
          type: object
          required: [employer_id, pay_period_start, pay_period_end, payment_date]
          properties:
            employer_id: { type: integer }
            pay_period_start: { type: string, format: date }
            pay_period_end: { type: string, format: date }
            payment_date: { type: string, format: date }
            pay_frequency: { type: string, enum: [weekly, fortnightly, monthly, quarterly] }
            event_type: { type: string, enum: [submit, update] }
            is_finalisation: { type: boolean }
            items:
              type: array
              items: { $ref: '#/components/schemas/PayEventItemInput' }

    PayEventItemInput:
      type: object
      required: [employee_id]
      properties:
        employee_id: { type: integer }
        gross_payment: { type: number }
        ordinary_time_earnings: { type: number }
        overtime: { type: number }
        bonuses_commissions: { type: number }
        payg_withholding: { type: number }
        super_guarantee: { type: number }
        super_salary_sacrifice: { type: number }
        qualifying_earnings:
          type: number
          description: |
            Payday Super (from 1 July 2026). SG contribution base per
            Qualifying Earnings rules. Leave as 0 for pre-Payday-Super events.
        annual_leave: { type: number }
        lump_sum_a: { type: number }
        lump_sum_b: { type: number }
        lump_sum_d: { type: number }
        lump_sum_e: { type: number }
        lump_sum_w: { type: number }
        rfba_exempt: { type: number }
        rfba_non_exempt: { type: number }

    Event:
      type: object
      properties:
        id: { type: string, example: evt_4f1c2b... }
        event_type:
          type: string
          enum:
            - submission.pending
            - submission.sent
            - submission.accepted
            - submission.rejected
            - submission.error
        status: { type: string, enum: [pending, delivering, succeeded, failed, abandoned] }
        attempts: { type: integer }
        created_at: { type: string, format: date-time }
        delivered_at: { type: string, format: date-time, nullable: true }
        webhook_endpoint_id: { type: integer }

    EventDetail:
      allOf:
        - $ref: '#/components/schemas/Event'
        - type: object
          properties:
            payload: { type: object, additionalProperties: true }
            response_code: { type: integer, nullable: true }
            response_body: { type: string, nullable: true }
            last_error: { type: string, nullable: true }
            replayed_from_id: { type: integer, nullable: true }
            next_retry_at: { type: string, format: date-time, nullable: true }

    YtdResponse:
      type: object
      properties:
        employer_id: { type: integer }
        financial_year: { type: string }
        totals:
          type: object
          additionalProperties: { type: number }
        employees:
          type: array
          items:
            type: object
            properties:
              employee_id: { type: integer }
              gross_payment: { type: number }
              payg_withholding: { type: number }
              super_guarantee: { type: number }
              qualifying_earnings: { type: number }
              finalised: { type: boolean }
