openapi: 3.1.0
info:
  title: ParseBank API
  version: "1.0"
  description: |
    Convert bank and credit card statements (PDFs, scans, photos) into
    structured, balance-verified transaction data.

    ## Quickstart

    1. Create an API key at `/dashboard/api` (Business plan or higher).
    2. Upload a statement:

    ```bash
    curl -X POST https://parsebank.io/api/v1/statements \
      -H "Authorization: Bearer sk_live_..." \
      -F "file=@statement.pdf"
    ```

    3. Poll `GET /api/v1/statements/{id}` until `status` is `completed`.
    4. Download the result via `GET /api/v1/statements/{id}/export?format=csv`.

    ## Conventions

    - All JSON responses are wrapped: `{ "data": … }` on success,
      `{ "error": { "code", "message" } }` on failure.
    - Monetary amounts are **strings with exactly two decimals** ("1234.56"),
      never floats. `debit` is money out, `credit` is money in.
    - Dates are ISO `YYYY-MM-DD` strings.

    ## Rate limits & credits

    - 60 requests/minute per key (sliding window). Exceeding it returns
      `429` with a `Retry-After` header.
    - Conversions use your plan's page credits (1 page = 1 credit), shared
      with the web app. Credits are reserved when a job is accepted and
      refunded automatically if extraction fails — you are only ever
      charged for completed conversions.

    ## Data retention

    Original files are deleted from storage the moment page rendering
    completes. Extracted data is retained per your account's retention
    setting (7–365 days, or Never) and can be deleted at any time with
    `DELETE /api/v1/statements/{id}`. PDF passwords are used once in memory
    and never stored. Your data is never used to train AI models.
servers:
  - url: /api/v1
security:
  - bearerAuth: []
tags:
  - name: Statements
  - name: Account
paths:
  /statements:
    post:
      tags: [Statements]
      operationId: createStatement
      summary: Upload a statement for conversion
      description: |
        Accepts a PDF, JPEG, or PNG (max 50MB, 100 pages) as
        `multipart/form-data`. Conversion is asynchronous — poll
        `GET /statements/{id}` until `completed`.

        Send an optional `Idempotency-Key` header to make retries safe:
        replays within 24 hours return the original statement (with an
        `Idempotency-Replayed: true` response header) instead of creating a
        duplicate job.
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema: { type: string, maxLength: 200 }
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
                  description: The statement (PDF, JPEG, or PNG)
                password:
                  type: string
                  description: Password for protected PDFs — used once, never stored
      responses:
        "202":
          description: Job accepted
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id: { type: string }
                      status: { type: string, const: processing }
                      pages: { type: integer }
              example:
                data: { id: "0b8f6c1e-4b9d-4a2e-9c11-7e9f1d2a3b4c", status: processing, pages: 12 }
        "400": { $ref: "#/components/responses/InvalidRequest" }
        "401": { $ref: "#/components/responses/InvalidApiKey" }
        "402":
          description: Not enough page credits
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
              example:
                error: { code: insufficient_credits, message: "This 12-page document exceeds your remaining page credits." }
        "403": { $ref: "#/components/responses/PlanRequired" }
        "413":
          description: File over 50MB or over 100 pages
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
              example:
                error: { code: file_too_large, message: "Files must be 50MB or smaller." }
        "415":
          description: Not a PDF/JPEG/PNG
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
              example:
                error: { code: unsupported_format, message: "Upload a PDF, JPEG, or PNG." }
        "422":
          description: PDF password missing or incorrect
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
              examples:
                required:
                  value:
                    error: { code: password_required, message: "This PDF is password-protected. Pass the password in the \"password\" form field." }
                incorrect:
                  value:
                    error: { code: password_incorrect, message: "The supplied PDF password is incorrect." }
        "429": { $ref: "#/components/responses/RateLimited" }
    get:
      tags: [Statements]
      operationId: listStatements
      summary: List statements
      parameters:
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
        - name: cursor
          in: query
          description: "`next_cursor` from the previous page"
          schema: { type: string }
      responses:
        "200":
          description: Paginated list, newest first
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      statements:
                        type: array
                        items: { $ref: "#/components/schemas/StatementSummary" }
                      next_cursor:
                        type: [string, "null"]
        "401": { $ref: "#/components/responses/InvalidApiKey" }
        "403": { $ref: "#/components/responses/PlanRequired" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /statements/{id}:
    get:
      tags: [Statements]
      operationId: getStatement
      summary: Get status and extracted transactions
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: >
            Statement detail. While `status` is `processing`, includes
            `pages_done`. When `completed`, includes `transactions`,
            `account`, `statement_type`, and `validation`. When `failed`,
            includes an `error` object (code `extraction_failed`) — failed
            jobs are refunded automatically.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/StatementDetail" }
              example:
                data:
                  id: "0b8f6c1e-4b9d-4a2e-9c11-7e9f1d2a3b4c"
                  status: completed
                  filename: statement.pdf
                  pages: 2
                  created_at: "2026-07-11T10:00:00.000Z"
                  expires_at: "2026-10-09T10:00:00.000Z"
                  currency: USD
                  account: "4821 9937 0016"
                  statement_type: bank_statement
                  validation: { balance_check: passed }
                  transactions:
                    - { date: "2026-03-01", description: "ACME PAYROLL SALARY MAR", debit: null, credit: "3250.00", balance: "7773.17", currency: USD }
                    - { date: "2026-03-02", description: "CARD PURCHASE WHOLE FOODS", debit: "87.43", credit: null, balance: "7685.74", currency: USD }
        "401": { $ref: "#/components/responses/InvalidApiKey" }
        "403": { $ref: "#/components/responses/PlanRequired" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      tags: [Statements]
      operationId: deleteStatement
      summary: Delete extracted data immediately
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Deleted
          content:
            application/json:
              example: { data: { deleted: true } }
        "401": { $ref: "#/components/responses/InvalidApiKey" }
        "403": { $ref: "#/components/responses/PlanRequired" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /statements/{id}/export:
    get:
      tags: [Statements]
      operationId: exportStatement
      summary: Download the export file
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
        - name: format
          in: query
          schema: { type: string, enum: [csv, xlsx, json], default: csv }
      responses:
        "200":
          description: File download (Content-Disposition attachment)
          content:
            text/csv: { schema: { type: string } }
            application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:
              schema: { type: string, format: binary }
            application/json: { schema: { type: object } }
        "401": { $ref: "#/components/responses/InvalidApiKey" }
        "403": { $ref: "#/components/responses/PlanRequired" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409":
          description: Statement still processing or failed
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /me:
    get:
      tags: [Account]
      operationId: getMe
      summary: Current key's account
      responses:
        "200":
          description: Account info
          content:
            application/json:
              example:
                data: { email: "ops@firm.com", plan: business, credits_remaining: 431 }
        "401": { $ref: "#/components/responses/InvalidApiKey" }
        "403": { $ref: "#/components/responses/PlanRequired" }
        "429": { $ref: "#/components/responses/RateLimited" }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: "API key from /dashboard/api: `Authorization: Bearer sk_live_…`"
  schemas:
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              enum:
                - invalid_api_key
                - plan_required
                - insufficient_credits
                - file_too_large
                - unsupported_format
                - password_required
                - password_incorrect
                - extraction_failed
                - not_found
                - invalid_request
                - rate_limited
            message: { type: string }
    Money:
      type: [string, "null"]
      description: Amount with exactly two decimals, as a string ("1234.56")
      pattern: '^-?\d+\.\d{2}$'
    Transaction:
      type: object
      properties:
        date: { type: [string, "null"], description: ISO YYYY-MM-DD }
        description: { type: string }
        debit: { $ref: "#/components/schemas/Money" }
        credit: { $ref: "#/components/schemas/Money" }
        balance: { $ref: "#/components/schemas/Money" }
        currency: { type: string }
    StatementSummary:
      type: object
      properties:
        id: { type: string }
        status: { type: string, enum: [processing, completed, failed] }
        filename: { type: string }
        pages: { type: integer }
        created_at: { type: string, format: date-time }
        expires_at:
          type: [string, "null"]
          format: date-time
          description: null when retention is set to Never
    StatementDetail:
      allOf:
        - { $ref: "#/components/schemas/StatementSummary" }
        - type: object
          properties:
            pages_done:
              type: integer
              description: Present while processing
            currency: { type: string }
            account:
              type: [string, "null"]
              description: Account identifier printed on the statement
            statement_type:
              type: string
              enum: [bank_statement, credit_card_statement]
            validation:
              type: object
              properties:
                balance_check: { type: string, enum: [passed, failed, "n/a"] }
            transactions:
              type: array
              items: { $ref: "#/components/schemas/Transaction" }
            error:
              type: object
              description: Present when status is failed
              properties:
                code: { type: string, const: extraction_failed }
                message: { type: string }
  responses:
    InvalidApiKey:
      description: Missing, malformed, or revoked API key
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example:
            error: { code: invalid_api_key, message: "This API key is invalid or has been revoked." }
    PlanRequired:
      description: API access requires Business plan or higher
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example:
            error: { code: plan_required, message: "API access requires the Business plan or higher. Upgrade at /pricing." }
    NotFound:
      description: No statement with this id (or it belongs to another account)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example:
            error: { code: not_found, message: "No statement with this id." }
    RateLimited:
      description: Over 60 requests/minute — retry after the `Retry-After` seconds
      headers:
        Retry-After:
          schema: { type: integer }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example:
            error: { code: rate_limited, message: "Rate limit exceeded (60 requests/minute). Retry after 12s." }
