openapi: 3.1.0
info:
  title: ClearStaq Public API
  version: 1.0.0
  summary: Submit bank-statement PDFs and receive structured analysis results.
  description: |
    The ClearStaq public API lets external systems submit bank-statement PDFs
    for parsing + fraud analysis. Results are delivered asynchronously by
    signed webhook to a caller-provided `callback_url`. A polling endpoint
    (`GET /v1/analyses/{id}`) is available for integrations that cannot
    receive inbound webhooks.

    Authentication uses bearer tokens created at
    https://app.clearstaq.com/app/settings/integrations.
servers:
  - url: https://app.clearstaq.com
    description: Production
components:
  securitySchemes:
    BearerApiKey:
      type: http
      scheme: bearer
      bearerFormat: cg_live_*
  schemas:
    AnalysisQueued:
      type: object
      required: [id, status, created_at]
      properties:
        id:
          type: string
          description: Analysis identifier. Use it to poll or correlate with a webhook payload.
        status:
          type: string
          enum: [queued]
        client_ref:
          type: string
          nullable: true
        callback_id:
          type: string
        created_at:
          type: string
          format: date-time
    ClientProfileQueued:
      type: object
      required: [client_id, batch_id, documents]
      properties:
        client_id:
          type: string
          format: uuid
          description: ClearStaq client created for this submission. Visible in the caller's webapp and used to poll the profile.
        batch_id:
          type: string
          format: uuid
          description: Identifier for this multi-document submission. Echoed back in the `batch.completed` webhook payload.
        documents:
          type: array
          items:
            type: object
            required: [filename, status]
            properties:
              id:
                type: string
                format: uuid
                nullable: true
                description: Document identifier once queued; `null` when the file was rejected before queuing.
              filename:
                type: string
              status:
                type: string
                enum: [queued, failed]
              error:
                type: string
                description: Failure code. Present only when `status` is `failed`.
    ClientProfile:
      type: object
      required: [id, name, status, documents, counts]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
          nullable: true
          description: Client name. Derived from the dominant account holder once parsed when `client_name` was omitted.
        status:
          type: string
          description: Aggregate client status.
        profile:
          $ref: "#/components/schemas/ClientProfileRollup"
        documents:
          type: array
          items:
            $ref: "#/components/schemas/ClientDocument"
        counts:
          type: object
          required: [total, completed, failed]
          properties:
            total: { type: integer }
            completed: { type: integer }
            failed: { type: integer }
    ClientProfileRollup:
      type: object
      nullable: true
      description: Aggregated revenue / MCA / fraud rollup over the valid statements. `null` until computed asynchronously after the documents finish parsing.
      additionalProperties: true
    ClientDocument:
      type: object
      required: [id, filename, status]
      properties:
        id: { type: string, format: uuid }
        filename: { type: string }
        status: { type: string }
        bank_name: { type: string, nullable: true }
        period_start: { type: string, format: date, nullable: true }
        period_end: { type: string, format: date, nullable: true }
        fraud_score: { type: integer, nullable: true }
        failure_reason:
          type: string
          nullable: true
          description: |
            Why a statement was excluded from the profile, e.g.
            `missing_pages`, `not_a_bank_statement:<type>`, or another reason
            string. `null` when the document parsed successfully.
        pages_present: { type: integer, nullable: true }
        pages_expected: { type: integer, nullable: true }
        detected_bank: { type: string, nullable: true }
    WebhookEnvelope:
      type: object
      required: [id, event, status, created_at]
      properties:
        id:
          type: string
        event:
          type: string
          enum: [analysis.completed, analysis.failed, batch.completed]
        client_ref:
          type: string
          nullable: true
        status:
          type: string
          enum: [completed, failed]
        created_at:
          type: string
          format: date-time
        completed_at:
          type: string
          format: date-time
          nullable: true
        metadata:
          type: object
          additionalProperties: true
        result:
          $ref: "#/components/schemas/AnalysisResult"
        error:
          $ref: "#/components/schemas/AnalysisError"
    AnalysisResult:
      type: object
      nullable: true
      properties:
        bank_name: { type: string, nullable: true }
        account_holder_name: { type: string, nullable: true }
        account_number_last4: { type: string, nullable: true }
        currency: { type: string, nullable: true }
        period_start: { type: string, format: date, nullable: true }
        period_end: { type: string, format: date, nullable: true }
        starting_balance: { type: number, nullable: true }
        ending_balance: { type: number, nullable: true }
        average_balance: { type: number, nullable: true }
        total_deposits: { type: number, nullable: true }
        total_withdrawals: { type: number, nullable: true }
        true_revenue: { type: number, nullable: true }
        estimated_monthly_revenue: { type: number, nullable: true }
        number_of_deposits: { type: integer, nullable: true }
        number_of_withdrawals: { type: integer, nullable: true }
        number_of_nsf: { type: integer, nullable: true }
        number_of_negative_days: { type: integer, nullable: true }
        fraud_score: { type: integer, nullable: true }
        fraud_risk_level: { type: string, nullable: true }
        monthly_summary: { type: array, items: { type: object } }
        debt_outflows: { type: array, items: { type: object } }
        transactions:
          type: array
          items:
            type: object
            properties:
              date: { type: string, format: date, nullable: true }
              description: { type: string, nullable: true }
              amount: { type: number, nullable: true }
              type: { type: string, nullable: true }
              category: { type: string, nullable: true }
              running_balance: { type: number, nullable: true }
              flagged: { type: boolean, nullable: true }
    AnalysisError:
      type: object
      nullable: true
      properties:
        code: { type: string }
        message: { type: string }
    BatchCompleted:
      type: object
      required: [event, batch_id, client_id, org_id, counts, created_at]
      properties:
        event:
          type: string
          enum: [batch.completed]
        batch_id:
          type: string
          format: uuid
        client_id:
          type: string
          format: uuid
        org_id:
          type: string
          format: uuid
        counts:
          type: object
          required: [total, completed, failed]
          properties:
            total: { type: integer }
            completed: { type: integer }
            failed: { type: integer }
        profile:
          $ref: "#/components/schemas/ClientProfileRollup"
        created_at:
          type: string
          format: date-time
    ErrorResponse:
      type: object
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code: { type: string }
            message: { type: string }
security:
  - BearerApiKey: []
paths:
  /api/v1/analyses:
    post:
      summary: Submit a bank statement for analysis
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
                  description: PDF file, up to 25MB.
                callback_url:
                  type: string
                  format: uri
                  description: HTTPS URL that will receive the signed webhook payload when the analysis completes.
                client_ref:
                  type: string
                  description: Free-form reference stored on the callback and echoed back in the webhook payload.
                metadata:
                  type: string
                  description: JSON-encoded object; passed through unchanged to the webhook payload.
                clearstaq_ai_enabled:
                  type: boolean
                  default: false
                  description: |
                    When `true`, the analysis includes the ClearStaqAI visual fraud
                    second-pass (Google Gemini). Defaults to `false`; when omitted
                    or false, the response contains only the deterministic metadata
                    fraud score. The visual second-pass adds roughly 10s of
                    processing per file.
      responses:
        "202":
          description: Accepted for processing.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AnalysisQueued"
        "401": { description: Invalid or missing API key, content: { application/json: { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
        "402": { description: Plan excluded or insufficient credits, content: { application/json: { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
        "413": { description: File too large (>25MB) }
        "415": { description: Only application/pdf is accepted }
        "422": { description: Validation failed, content: { application/json: { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
        "502": { description: Parser unavailable }
  /api/v1/analyses/{id}:
    get:
      summary: Fetch analysis status or completed result
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Analysis envelope or status snapshot.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookEnvelope"
        "404": { description: Analysis not found for this API key }
  /api/v1/clients:
    post:
      summary: Create a client profile from one or more statements
      description: |
        Creates a ClearStaq client in the caller's org (visible in their
        webapp), parses every uploaded statement, and computes an aggregated
        client profile over the valid statements. Incomplete or non-bank
        statements are excluded from the profile and listed with a failure
        reason. Billed per document (same credits as `/api/v1/analyses`),
        scaled by file count. Requires the `analyses:write` scope.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: array
                  items:
                    type: string
                    format: binary
                  description: |
                    One or more PDF bank statements (repeatable `file` field).
                    1 to 25 files, max 25 MB each, `application/pdf` only.
                    May be combined with `files`.
                files:
                  type: array
                  items:
                    type: string
                    format: binary
                  description: Alternative repeatable field for PDF bank statements. May be combined with `file`.
                client_name:
                  type: string
                  maxLength: 200
                  description: Optional client name. When omitted, the client is named from the dominant account holder once parsed.
                clearstaq_ai_enabled:
                  type: boolean
                  default: false
                  description: |
                    When `true`, each analysis includes the ClearStaqAI visual
                    fraud second-pass (Google Gemini). Defaults to `false`.
      responses:
        "202":
          description: Accepted for processing. The client and per-document queue state are returned immediately.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ClientProfileQueued"
        "400": { description: "No files supplied (`no_files`) or the API key is not tied to a user (`no_user_context`)", content: { application/json: { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
        "401": { description: Invalid or missing API key, content: { application/json: { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
        "402": { description: "Plan excluded (`plan_excluded`), no Stripe customer (`no_stripe_customer`), or insufficient credits (`insufficient_credits`)", content: { application/json: { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
        "413": { description: "More than 25 files (`too_many_files`)", content: { application/json: { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
        "502": { description: "Every file failed to queue (`all_files_failed`)", content: { application/json: { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
  /api/v1/clients/{id}:
    get:
      summary: Fetch a client profile and per-document statuses
      description: |
        Returns the client, its aggregated profile, and per-document parse
        status. Poll until
        `counts.completed + counts.failed == counts.total` and `profile` is
        non-null — the profile is computed asynchronously shortly after the
        documents finish parsing. Requires the `analyses:read` scope.
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: Client profile snapshot.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ClientProfile"
        "400": { description: "Malformed client id (`invalid_id`)", content: { application/json: { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
        "404": { description: "Client not found for this API key (`not_found`)", content: { application/json: { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
  /api/v1/webhooks/test:
    post:
      summary: Send a signed test webhook to a URL
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [callback_url]
              properties:
                callback_url:
                  type: string
                  format: uri
      responses:
        "200":
          description: Delivered (or receiver responded).
          content:
            application/json:
              schema:
                type: object
                properties:
                  delivered: { type: boolean }
                  status: { type: integer, nullable: true }
                  signature: { type: string }
webhooks:
  analysisCompleted:
    post:
      summary: ClearStaq -> your server on analysis completion
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebhookEnvelope"
      responses:
        "2XX":
          description: Receipt acknowledged. Any non-2xx response triggers retry with exponential backoff.
  batchCompleted:
    post:
      summary: ClearStaq -> your server once a multi-document client profile is computed
      description: |
        Fires once per `/api/v1/clients` submission, after the client profile
        is computed. Signing (HMAC `X-ClearStaq-Signature`) and the 5-step
        retry backoff are identical to `analysis.completed`. Subscribe with
        `event_type: "batch.completed"` via `POST /api/v1/webhooks`.
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BatchCompleted"
      responses:
        "2XX":
          description: Receipt acknowledged. Any non-2xx response triggers retry with exponential backoff.
