openapi: 3.1.0
info:
  title: Fylit Pro packet API
  version: "1"
  description: |
    JSON in, filled PDF out: Form 5472 and the pro forma Form 1120 for a foreign-owned US single-member
    LLC (a disregarded entity), built by the same engine as Fylit Pro's client intake.

    - Test keys (fyl_test_...) return a sample marked TEST, NOT FOR FILING on every page and never use a
      credit.
    - Live keys (fyl_live_...) return the real packet with your firm's name on the cover and use one
      credit. Live keys come with the Firm + API plan.
    - Nothing is kept. The request is validated and rendered inside a transaction that is rolled back;
      Fylit stores only who called, when, the status, the tax year, the page count and a SHA-256 of the
      request body (for idempotency). The PDF exists only in the response.
    - Your firm reviews, signs and files every return prepared with Fylit. Fylit does not give tax or
      legal advice and does not file anything with the IRS.

    The request body has the same fields as the intake the Fylit Pro wizard collects. Which fields are
    required depends on the case (for example Part V and loan details); a request the engine cannot
    complete is answered 422 with each blocking issue.
servers:
  - url: https://api.fylit.tax
paths:
  /api/v1/packets:
    post:
      summary: Generate a packet
      operationId: createPacket
      security:
        - apiKey: []
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          description: >-
            Retry safely. A live request repeated with the same key and the same body is rendered again
            and not charged again. The same key with a different body is refused (422). A request that
            failed does not use up its key.
          schema:
            type: string
            maxLength: 255
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PacketRequest'
            example:
              {
                "entity_name": "Acme LLC",
                "ein": "12-3456789",
                "owner_name": "Marie Dupont",
                "owner_country": "France",
                "tax_year": 2025,
                "entity_address_street": "100 Main St, Suite 4",
                "entity_address_city": "Dover",
                "entity_address_state": "DE",
                "entity_address_zip": "19901",
                "entity_total_assets_cents": 1234500,
                "entity_business_activity": "Software consulting",
                "entity_business_activity_code": "541511",
                "entity_date_incorporated": "2023-04-17",
                "entity_principal_business_countries": "United States",
                "first_year_filing": false,
                "reporting_corp_tax_residence": "United States",
                "rp_related_to_reporting_corp": false,
                "rp_related_to_25pct_shareholder": false,
                "rp_is_25pct_shareholder": true,
                "item_e_initial_return": false,
                "item_e_final_return": false,
                "item_e_name_change": false,
                "item_e_address_change": false,
                "part_v_in_line_1f": false,
                "part_vii_imports_goods": false,
                "part_vii_csa_participant": false,
                "part_vii_267a_disallowed": false,
                "part_vii_fdii_claimed": false,
                "part_vii_safe_haven_within": false,
                "part_vii_safe_haven_outside": false,
                "part_vi_present": false,
                "part_ix_base_erosion_payments_cents": 0,
                "part_ix_base_erosion_tax_benefits_cents": 0,
                "part_ix_qualified_derivative_payments_cents": 0,
                "owner_address_street": "12 Rue de Rivoli",
                "owner_address_city": "Paris",
                "owner_address_postal_code": "75001",
                "owner_ftin": "F123",
                "owner_country_citizenship": "France",
                "owner_country_tax_residence": "France",
                "owner_principal_business_countries": "France",
                "owner_business_activity": "Investor",
                "owner_business_activity_code": "523900",
                "transactions": [
                  {
                    "category": "capital_contribution",
                    "description": "Seed",
                    "amount_cents": 500000,
                    "occurred_on": "2025-03-01"
                  }
                ]
              }
      responses:
        "200":
          description: The packet.
          headers:
            Fylit-Mode:
              schema: {type: string, enum: [test, live]}
            Fylit-Charged:
              description: Whether this request used a credit.
              schema: {type: string, enum: ["true", "false"]}
            Fylit-Idempotent-Replay:
              schema: {type: string, enum: ["true", "false"]}
            Fylit-Credits-Remaining:
              description: Live keys only.
              schema: {type: string}
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        "400":
          description: The body is not valid (code invalid_request, with field errors).
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Error'}
        "401":
          description: Missing, invalid, revoked or expired API key. An expired key says so; rotate keys before they expire.
        "402":
          description: No packet credits left (code no_credits). Live keys only.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Error'}
        "403":
          description: >-
            The key may not do this now. Codes: ip_not_allowed (the call came from outside the key's address
            list), insufficient_scope (the key lacks the packets scope), live_access_ended (a live key, and the
            firm has neither a Firm + API plan nor a license in force; test keys still work), firm_suspended,
            or terms_not_accepted (the firm's owner has not accepted the Pro terms now in force). Nothing is
            charged. Sign in to Fylit Pro to resolve the last two.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Error'}
        "410":
          description: Fylit Pro is not available.
        "422":
          description: >-
            The engine cannot build this packet (code validation_blocked, with validation_issues), the
            case is outside what Fylit supports (not_renderable), or the Idempotency-Key was reused with
            a different body (idempotency_key_reused). Nothing is charged.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Error'}
        "429":
          description: >-
            More than 60 requests a minute on one key (packets, batches and usage share the limit). The
            Retry-After header says how many seconds to wait. Nothing is charged.
          headers:
            Retry-After:
              schema: {type: integer}
        "500":
          description: >-
            The renderer failed (code render_failed). Nothing is charged, nothing is kept, and the
            Idempotency-Key stays usable, so the same request can be sent again. If it keeps failing,
            email support with the time of the request.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Error'}
  /api/v1/packet-batches:
    post:
      summary: Generate several packets in one call
      description: >-
        Up to 10 packet requests, rendered one after another inside this request and returned as a ZIP of PDFs and
        a results.json. It is synchronous on purpose: nothing is stored, so there is nothing to fetch later, which
        keeps the zero-retention promise (there are no webhooks and no results to poll). Each item is validated,
        rendered and charged exactly as a single request is, and one item's error never stops the others. Send
        an Idempotency-Key to make the whole batch safe to retry: item N uses "<key>:N", so a retry charges only for
        packets not yet made.
      operationId: createPacketBatch
      security:
        - apiKey: []
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/BatchRequest'}
      responses:
        "200":
          description: >-
            A ZIP holding packet-NN.pdf (or packet-NN-<client_ref>.pdf) for each item that was made, and
            results.json (see BatchResults) for every item. Headers say how many succeeded and were charged.
          headers:
            Fylit-Batch-Items: {schema: {type: string}}
            Fylit-Batch-Succeeded: {schema: {type: string}}
            Fylit-Batch-Failed: {schema: {type: string}}
            Fylit-Charged:
              description: How many credits this batch used.
              schema: {type: string}
            Fylit-Credits-Remaining:
              description: Live keys only.
              schema: {type: string}
          content:
            application/zip:
              schema: {type: string, format: binary}
        "400":
          description: >-
            The batch itself is malformed: invalid_request (no items, not a list, a bad or repeated client_ref) or
            batch_too_large. A malformed single item is an item error in results.json, not this.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Error'}
        "401":
          description: Missing, invalid, revoked or expired API key.
        "403":
          description: As for a single packet (ip_not_allowed, insufficient_scope, live_access_ended, firm_suspended, terms_not_accepted).
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Error'}
        "422":
          description: >-
            No packet in the batch could be made (code batch_failed, with each item's error in items), or the
            Idempotency-Key was reused with a different batch (idempotency_key_reused). Nothing is charged.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Error'}
        "429":
          description: Shares the packet limit; see Retry-After.
  /api/v1/usage:
    get:
      summary: Read the firm's API usage
      description: >-
        The firm's API calls, newest first: who, when, the status, tax year, pages and whether a credit was used.
        Never the request or the PDF. Needs the usage scope.
      operationId: getUsage
      security:
        - apiKey: []
      parameters:
        - {name: key, in: query, required: false, description: Only calls made with the key with this prefix., schema: {type: string}}
        - {name: from, in: query, required: false, description: First day, inclusive (UTC, YYYY-MM-DD)., schema: {type: string, format: date}}
        - {name: to, in: query, required: false, description: Last day, inclusive (UTC, YYYY-MM-DD)., schema: {type: string, format: date}}
        - {name: limit, in: query, required: false, description: "1 to 1000 (default 100); for output=csv up to 10000 (default 10000).", schema: {type: integer}}
        - {name: before, in: query, required: false, description: The next value from the previous page., schema: {type: string}}
        - {name: output, in: query, required: false, description: json (default) or csv., schema: {type: string, enum: [json, csv]}}
      responses:
        "200":
          description: One page of usage, or a CSV with the same columns.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/UsageResponse'}
            text/csv:
              schema: {type: string}
        "400":
          description: A filter is not valid (code invalid_request).
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Error'}
        "401":
          description: Missing, invalid, revoked or expired API key.
        "403":
          description: ip_not_allowed or insufficient_scope (the key lacks the usage scope).
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Error'}
        "429":
          description: See Retry-After.
components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: "Authorization: Bearer fyl_test_... or fyl_live_..."
  schemas:
    BatchRequest:
      type: object
      required: [items]
      properties:
        items:
          type: array
          minItems: 1
          maxItems: 10
          items:
            allOf:
              - $ref: '#/components/schemas/PacketRequest'
              - type: object
                properties:
                  client_ref:
                    type: string
                    pattern: "^[A-Za-z0-9._-]{1,64}$"
                    description: Your own label for the item, unique in the batch. Echoed in results.json and used in the file name.
    BatchResults:
      type: object
      properties:
        succeeded: {type: integer}
        failed: {type: integer}
        charged: {type: integer}
        items:
          type: array
          items:
            type: object
            properties:
              index: {type: integer}
              client_ref: {type: [string, "null"]}
              status: {type: string, enum: [ok, error]}
              file: {type: string, description: The PDF's name in the ZIP (status ok).}
              pages: {type: integer}
              charged: {type: boolean}
              code: {type: string, description: "On error: invalid_request, validation_blocked, not_renderable, no_credits, idempotency_key_reused or render_failed."}
              detail: {type: string}
              errors: {type: object}
              validation_issues: {type: array, items: {type: object}}
    UsageResponse:
      type: object
      properties:
        results:
          type: array
          items:
            type: object
            properties:
              id: {type: integer}
              key_prefix: {type: string}
              key_name: {type: string}
              mode: {type: string, enum: [test, live]}
              status_code: {type: integer}
              tax_year: {type: [integer, "null"]}
              pages: {type: [integer, "null"]}
              charged: {type: boolean}
              duration_ms: {type: integer}
              created_at: {type: string, format: date-time}
        next: {type: [string, "null"], description: Pass as before for the next page; null when there is no more.}
    PacketRequest:
      type: object
      required: [entity_name, ein, owner_name, owner_country, tax_year, transactions]
      properties:
        entity_name:
          type: string
        ein:
          type: string
          description: Never stored by the API; used only to fill this packet.
        owner_name:
          type: string
        owner_country:
          type: string
        tax_year:
          type: integer
        screener_outcome:
          type: string
          enum: [eligible, eligible_complex]
        transactions:
          type: array
          maxItems: 50
          items:
            $ref: '#/components/schemas/Transaction'
        entity_classification:
          type: string
          enum: [disregarded, c_corp, s_corp, partnership]
        election_filed:
          type: boolean
        ustob_flag:
          type: boolean
        eci_flag:
          type: boolean
        multi_member:
          type: boolean
        withholding_cert_type:
          type: string
        entity_address_street:
          type: string
        entity_address_city:
          type: string
        entity_address_state:
          type: string
        entity_address_zip:
          type: string
        entity_total_assets_cents:
          type: integer
          description: US dollars in cents.
        entity_business_activity:
          type: string
        entity_business_activity_code:
          type: string
        entity_date_incorporated:
          type: string
          format: date
        entity_principal_business_countries:
          type: string
        first_year_filing:
          type: boolean
        reporting_corp_tax_residence:
          type: string
        rp_related_to_reporting_corp:
          type: boolean
        rp_related_to_25pct_shareholder:
          type: boolean
        rp_is_25pct_shareholder:
          type: boolean
        item_e_initial_return:
          type: boolean
        item_e_final_return:
          type: boolean
        item_e_name_change:
          type: boolean
        item_e_address_change:
          type: boolean
        part_v_in_line_1f:
          type: boolean
        part_vii_imports_goods:
          type: boolean
        part_vii_basis_over_customs:
          type: boolean
        part_vii_import_docs_available:
          type: boolean
        part_vii_csa_participant:
          type: boolean
        part_vii_267a_disallowed:
          type: boolean
        part_vii_267a_amount_cents:
          type: integer
          description: US dollars in cents.
        part_vii_fdii_claimed:
          type: boolean
        part_vii_fdii_general_property_cents:
          type: integer
          description: US dollars in cents.
        part_vii_fdii_intangible_property_cents:
          type: integer
          description: US dollars in cents.
        part_vii_fdii_services_cents:
          type: integer
          description: US dollars in cents.
        part_vii_safe_haven_within:
          type: boolean
        part_vii_safe_haven_outside:
          type: boolean
        part_vii_38b_explanation:
          type: string
          maxLength: 4000
          description: Explanation for line 38b when applicable.
        part_vi_present:
          type: [boolean, "null"]
        part_vi_description:
          type: string
          maxLength: 4000
        part_vi_amount_cents:
          type: [integer, "null"]
          description: US dollars in cents.
        part_viii_csa_description:
          type: string
          maxLength: 4000
        part_viii_became_participant:
          type: [boolean, "null"]
        part_viii_csa_before_2009:
          type: [boolean, "null"]
        part_viii_rab_share:
          type: string
          maxLength: 16
          description: Reasonably anticipated benefits share as a percentage from 0 to 100.
        part_viii_sbc_deductions_cents:
          type: [integer, "null"]
          description: US dollars in cents.
        part_viii_sbc_ida_cents:
          type: [integer, "null"]
          description: US dollars in cents.
        part_viii_sbc_not_allocable:
          type: [boolean, "null"]
        part_viii_idc_total_cents:
          type: [integer, "null"]
          description: US dollars in cents.
        part_viii_idc_allocable_cents:
          type: [integer, "null"]
          description: US dollars in cents.
        part_ix_base_erosion_payments_cents:
          type: [integer, "null"]
          description: Line 50, US dollars in cents. Enter 0 if none; null blocks generation.
        part_ix_base_erosion_tax_benefits_cents:
          type: [integer, "null"]
          description: Line 51, US dollars in cents. Enter 0 if none; null blocks generation.
        part_ix_qualified_derivative_payments_cents:
          type: [integer, "null"]
          description: Line 52, US dollars in cents. Enter 0 if none; null blocks generation.
        owner_address_street:
          type: string
        owner_address_city:
          type: string
        owner_address_province_state:
          type: string
        owner_address_postal_code:
          type: string
        owner_us_tin:
          type: string
          description: Never stored by the API; used only to fill this packet.
        owner_ftin:
          type: string
          description: Never stored by the API; used only to fill this packet.
        owner_country_citizenship:
          type: string
        owner_country_tax_residence:
          type: string
        owner_principal_business_countries:
          type: string
        owner_business_activity:
          type: string
        owner_business_activity_code:
          type: string
    Transaction:
      type: object
      required: [category, amount_cents]
      properties:
        category:
          type: string
          description: >-
            The retired single slugs loan_related and royalty_or_service are not accepted: use loan_borrowed or
            loan_loaned, and the specific royalty, IP license or service category.
          enum: [capital_contribution, distribution, royalty_received, royalty_paid, ip_license_received, ip_license_paid, service_received, service_paid, loan_borrowed, loan_loaned, inventory_sold, inventory_purchased, tangible_property_sold, tangible_property_purchased, rent_received, rent_paid, commission_received, commission_paid, interest_received, interest_paid, insurance_premium_received, insurance_premium_paid, loan_guarantee_fee_received, loan_guarantee_fee_paid, other_received, other_paid]
        description:
          type: string
        amount_cents:
          type: integer
          description: US dollars in cents.
        direction:
          type: string
          enum: [paid, received]
        related_party:
          type: string
        is_formation_cost:
          type: boolean
        is_deemed_contribution:
          type: boolean
        occurred_on:
          type: string
          format: date
        beginning_balance_cents:
          type: integer
          description: US dollars in cents.
        balance_method:
          type: string
          enum: [ending, monthly_average]
    Error:
      type: object
      required: [code, detail]
      properties:
        code:
          type: string
        detail:
          type: string
        errors:
          type: object
          description: Field errors (invalid_request).
        validation_issues:
          type: array
          items:
            type: object
            required: [code, message]
            properties:
              code: {type: string}
              message: {type: string}
              severity: {type: string}
              citation: {type: string}
