openapi: 3.1.0
info:
  title: TrustBarrier API
  version: 1.0.0
  description: |
    Pre-charge fraud screening API for Aruba merchants. See
    https://github.com/innolinkholdings/aruba-fraud-shield/docs for the full
    integration guide.

    Two endpoints carry the network effect: `POST /v1/check` (consume) and
    `POST /v1/report` (contribute). All other endpoints support these.
  contact:
    name: TrustBarrier support
    email: support@trustbarrier.tech
  license:
    name: Proprietary
servers:
  - url: https://api.trustbarrier.tech/v1
    description: Production
security:
  - ApiKey: []
tags:
  - name: Decision
    description: The pre-charge fraud screen
  - name: Reporting
    description: Submit known-bad signatures (the network-effect endpoints)
  - name: Blocklist
    description: Manage merchant blocklist entries
  - name: Events
    description: Audit + analytics over /check decisions
  - name: Stats
    description: Aggregate counters
  - name: Identity
    description: Echo the calling key

paths:
  /check:
    post:
      operationId: check
      tags: [Decision]
      summary: Pre-charge fraud screen
      description: |
        Returns `block` or `allow` based on the merchant's blocklist plus
        (optionally) the global pool. Latency target p95 < 50ms.

        At least one identifier (`ip`, `address`, `email`, `phone`, `card`,
        `delivery_lat`/`lng`, `device_fingerprint`, or `name`) is required.
        Send everything you have; the engine matches against what's present
        and captures the rest into the audit trail for analytics. Today the
        engine matches on `ip` and `address`; the additional identifiers
        are accepted in the request shape (forward-compatible) and will
        feed the matcher in phase 2 without a breaking change.

        Callers MUST set a hard timeout (recommended 2 seconds) and MUST
        fail open on any non-200 response or timeout.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CheckRequest'
      responses:
        '200':
          description: Decision
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CheckResponse'
        '422':
          $ref: '#/components/responses/InvalidInput'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'

  /report:
    post:
      operationId: report
      tags: [Reporting]
      summary: Manually or automatically submit a known-bad signature
      description: |
        Two flows hit this endpoint:

        1. **Manual** — a CS rep clicks "Report fraud" in the merchant
           dashboard or in the merchant's own admin panel. `reported_via`
           is `manual_dashboard` or `manual_api`.
        2. **Automated** — the merchant's chargeback webhook handler calls
           this. `reported_via` is `automated_chargeback` or `automated_other`.

        With `share_globally=true`, the entry enters a review queue for the
        global pool. After approval (auto or platform-admin manual), other
        merchants benefit from the signature on subsequent /check calls.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReportRequest'
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BlocklistEntry'
        '409':
          description: Duplicate
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
        '422':
          description: Invalid input or protected ISP rejection
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'

  /blocklist:
    get:
      operationId: listBlocklist
      tags: [Blocklist]
      summary: List the merchant's blocklist entries
      parameters:
        - { in: query, name: type, schema: { type: string, enum: [ip, address] } }
        - { in: query, name: active, schema: { type: boolean, default: true } }
        - { in: query, name: query, schema: { type: string } }
        - { in: query, name: created_since, schema: { type: string, format: date-time } }
        - { in: query, name: min_hits, schema: { type: integer, minimum: 0 } }
        - { in: query, name: page, schema: { type: integer, minimum: 1, default: 1 } }
        - { in: query, name: per_page, schema: { type: integer, minimum: 1, maximum: 200, default: 50 } }
        - { in: query, name: sort, schema: { type: string, default: '-created_at' } }
      responses:
        '200':
          description: Paginated list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/BlocklistEntry' }
                  meta:
                    $ref: '#/components/schemas/PaginationMeta'

  /blocklist/ip:
    post:
      operationId: addIpEntry
      tags: [Blocklist]
      summary: Add a single IP/CIDR entry directly
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [value]
              properties:
                value:           { type: string, description: IP or CIDR }
                reason:          { type: string, maxLength: 500 }
                share_globally:  { type: boolean, default: false }
                expires_at:      { type: string, format: date-time, nullable: true }
                evidence_reference: { type: string, maxLength: 255 }
                reported_via:
                  type: string
                  enum: [manual_dashboard, manual_api, automated_chargeback, automated_other]
                  default: manual_api
      responses:
        '201': { description: Created, content: { application/json: { schema: { $ref: '#/components/schemas/BlocklistEntry' } } } }
        '422': { description: Invalid input or protected ISP, content: { application/json: { schema: { $ref: '#/components/schemas/Problem' } } } }

  /blocklist/address:
    post:
      operationId: addAddressEntry
      tags: [Blocklist]
      summary: Add a single address entry directly
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [value]
              properties:
                value:           { type: string, description: Free-form delivery address }
                reason:          { type: string, maxLength: 500 }
                share_globally:  { type: boolean, default: false }
                expires_at:      { type: string, format: date-time, nullable: true }
                evidence_reference: { type: string, maxLength: 255 }
                reported_via:
                  type: string
                  enum: [manual_dashboard, manual_api, automated_chargeback, automated_other]
                  default: manual_api
      responses:
        '201': { description: Created, content: { application/json: { schema: { $ref: '#/components/schemas/BlocklistEntry' } } } }
        '422': { description: Invalid input, content: { application/json: { schema: { $ref: '#/components/schemas/Problem' } } } }

  /blocklist/ip/{id}:
    delete:
      operationId: removeIpEntry
      tags: [Blocklist]
      summary: Remove an IP blocklist entry
      parameters:
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '204': { description: Removed }
        '404': { description: Not found, content: { application/json: { schema: { $ref: '#/components/schemas/Problem' } } } }

  /blocklist/address/{id}:
    delete:
      operationId: removeAddressEntry
      tags: [Blocklist]
      summary: Remove an address blocklist entry
      parameters:
        - { in: path, name: id, required: true, schema: { type: integer } }
      responses:
        '204': { description: Removed }
        '404': { description: Not found, content: { application/json: { schema: { $ref: '#/components/schemas/Problem' } } } }

  /health:
    get:
      operationId: getHealth
      tags: [Identity]
      summary: Public health probe (no auth)
      security: []
      responses:
        '200':
          description: All dependencies healthy
          content:
            application/json:
              schema:
                type: object
                required: [status, version, timestamp, checks]
                properties:
                  status: { type: string, enum: [ok, degraded] }
                  version: { type: string, example: "0.1.0-dev" }
                  timestamp: { type: string, format: date-time }
                  checks:
                    type: object
                    properties:
                      database: { type: string, enum: [ok, fail] }
                      redis:    { type: string, enum: [ok, fail] }
                      queue:    { type: string, enum: [ok, fail] }
        '503':
          description: One or more dependencies unhealthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [degraded] }

  /events:
    get:
      operationId: listEvents
      tags: [Events]
      summary: Recent /check decisions
      parameters:
        - { in: query, name: since, schema: { type: string, format: date-time } }
        - { in: query, name: until, schema: { type: string, format: date-time } }
        - { in: query, name: decision, schema: { type: string, enum: [block, allow] } }
        - { in: query, name: matched_type, schema: { type: string, enum: [ip, address] } }
        - { in: query, name: reference_id, schema: { type: string } }
        - { in: query, name: page, schema: { type: integer, default: 1 } }
        - { in: query, name: per_page, schema: { type: integer, default: 50, maximum: 200 } }
      responses:
        '200':
          description: Paginated event list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/BlockEvent' }
                  meta:
                    $ref: '#/components/schemas/PaginationMeta'

  /stats:
    get:
      operationId: getStats
      tags: [Stats]
      parameters:
        - { in: query, name: period, schema: { type: string, enum: [24h, 7d, 30d, 90d], default: 30d } }
      responses:
        '200':
          description: Aggregate counters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Stats'

  /me:
    get:
      operationId: getIdentity
      tags: [Identity]
      summary: Echo the calling key + merchant
      responses:
        '200':
          description: Identity
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Identity'

components:
  securitySchemes:
    ApiKey:
      type: http
      scheme: bearer
      bearerFormat: API key (afs_<env>_<32chars>)

  parameters:
    IdempotencyKey:
      in: header
      name: Idempotency-Key
      schema: { type: string }
      description: |
        Optional UUID. When supplied, repeat calls within 24 hours with the
        same merchant + key + body return the original response without
        creating a duplicate row.

  schemas:
    CheckRequest:
      type: object
      description: |
        At least one identifier field is required. Send everything you have;
        the engine matches what it can today and captures the rest for analytics
        + future matching.
      properties:
        ip:                 { type: string, format: ipv4, description: "IPv4 or IPv6 address of the customer" }
        address:            { type: string, maxLength: 500, description: "Free-form delivery address (normalized server-side: lowercase + strip non-alphanumeric)" }
        email:              { type: string, format: email, maxLength: 255 }
        phone:              { type: string, maxLength: 32, pattern: '^\+?[0-9\-\s().]{7,32}$', description: "E.164-ish; we normalize" }
        name:               { type: string, maxLength: 255 }
        delivery_lat:       { type: number, format: float, minimum: -90, maximum: 90 }
        delivery_lng:       { type: number, format: float, minimum: -180, maximum: 180 }
        device_fingerprint: { type: string, maxLength: 255, description: "Opaque device fingerprint hash; provider-specific format" }
        card:
          type: object
          description: "Masked card fingerprint. PCI-clean — never include full PAN, CVV, or track data. The shape your PSP already exposes."
          required: [brand, bin, last4]
          properties:
            brand:     { type: string, enum: [visa, mastercard, amex, discover, diners, jcb, unionpay, maestro, other] }
            bin:       { type: string, pattern: '^[0-9]{6}$', description: "First 6 digits of PAN" }
            last4:     { type: string, pattern: '^[0-9]{4}$' }
            exp_month: { type: integer, minimum: 1, maximum: 12 }
            exp_year:  { type: integer, minimum: 2024, maximum: 2099 }
        reference_id: { type: string, maxLength: 120, description: "Your order ID / ticket ID — echoes back on the event row" }
        metadata:     { type: object, additionalProperties: true, description: "Arbitrary key-values you want surfaced on the event row for forensics" }
      anyOf:
        - { required: [ip] }
        - { required: [address] }
        - { required: [email] }
        - { required: [phone] }
        - { required: [card] }
        - { required: [device_fingerprint] }
        - { required: [delivery_lat, delivery_lng] }

    CheckResponse:
      type: object
      required: [decision, reason_codes, event_id]
      properties:
        decision:
          type: string
          enum: [block, allow]
        reason_codes:
          type: array
          items:
            type: string
            enum: [ip_blocked, ip_blocked_cidr, address_blocked, merchant_suspended]
        matched:
          type: object
          nullable: true
          properties:
            type:                { type: string, enum: [ip, address] }
            scope:               { type: string, enum: [local, global] }
            added_at:            { type: string, format: date-time }
            evidence_reference:  { type: string, nullable: true }
        event_id:                { type: string }

    ReportRequest:
      type: object
      required: [type, value, reported_via, share_globally]
      properties:
        type:           { type: string, enum: [ip, address] }
        value:          { type: string, description: IP, CIDR, or address text }
        reason:         { type: string, maxLength: 500 }
        reported_via:
          type: string
          enum: [manual_dashboard, manual_api, automated_chargeback, automated_other]
        share_globally: { type: boolean }
        expires_at:     { type: string, format: date-time, nullable: true }
        evidence_reference: { type: string, maxLength: 255 }

    BlocklistEntry:
      type: object
      properties:
        id:                       { type: string }
        type:                     { type: string, enum: [ip, address] }
        value:                    { type: string }
        normalized_value:         { type: string }
        is_cidr:                  { type: boolean }
        scope:                    { type: string, enum: [merchant, global] }
        reason:                   { type: string, nullable: true }
        reported_via:
          type: string
          enum: [manual_dashboard, manual_api, automated_chargeback, automated_other]
        shared_with_global:       { type: boolean }
        shared_globally_pending_review: { type: boolean }
        evidence_reference:       { type: string, nullable: true }
        expires_at:               { type: string, format: date-time, nullable: true }
        hit_count:                { type: integer }
        last_hit_at:              { type: string, format: date-time, nullable: true }
        created_by:
          type: object
          nullable: true
          properties:
            type: { type: string, enum: [merchant_admin, platform_admin, system] }
            id:   { type: integer }
            name: { type: string }
        created_at:               { type: string, format: date-time }

    BlockEvent:
      type: object
      properties:
        id:              { type: string }
        occurred_at:     { type: string, format: date-time }
        decision:        { type: string, enum: [block, allow] }
        matched_type:    { type: string, enum: [ip, address], nullable: true }
        matched_scope:   { type: string, enum: [local, global], nullable: true }
        request_ip:      { type: string, nullable: true }
        request_address: { type: string, nullable: true }
        reference_id:    { type: string, nullable: true }
        metadata:        { type: object, additionalProperties: true, nullable: true }
        api_key_prefix:  { type: string }

    Stats:
      type: object
      properties:
        period:                    { type: string }
        checks_total:              { type: integer }
        checks_blocked:            { type: integer }
        block_rate:                { type: number }
        estimated_savings_awg:     { type: number }
        monthly_cost_awg:          { type: number }
        roi_multiple:              { type: number }
        top_blocked_ips:           { type: array, items: { type: object, properties: { value: { type: string }, hits: { type: integer } } } }
        top_blocked_addresses:     { type: array, items: { type: object, properties: { value: { type: string }, hits: { type: integer } } } }
        reports_submitted:         { type: integer }
        reports_shared_globally:   { type: integer }
        global_pool_hits_received: { type: integer }

    Identity:
      type: object
      properties:
        merchant:
          type: object
          properties:
            id:     { type: string }
            name:   { type: string }
            tier:   { type: string }
            status: { type: string }
        api_key:
          type: object
          properties:
            id:     { type: string }
            label:  { type: string }
            prefix: { type: string }
            scopes: { type: array, items: { type: string } }
        quotas:
          type: object
          properties:
            checks_used:         { type: integer }
            checks_limit:        { type: integer }
            reports_used:        { type: integer }
            reports_limit:       { type: integer }
            period_resets_at:    { type: string, format: date-time }

    PaginationMeta:
      type: object
      properties:
        pagination:
          type: object
          properties:
            page:     { type: integer }
            per_page: { type: integer }
            total:    { type: integer }
            pages:    { type: integer }

    Problem:
      type: object
      description: RFC 7807 Problem Details
      properties:
        type:    { type: string, format: uri }
        title:   { type: string }
        status:  { type: integer }
        detail:  { type: string }
        errors:
          type: object
          additionalProperties:
            type: array
            items: { type: string }

  responses:
    InvalidInput:
      description: Validation failed
      content: { application/json: { schema: { $ref: '#/components/schemas/Problem' } } }
    RateLimited:
      description: Rate limit exceeded
      headers:
        X-RateLimit-Limit:     { schema: { type: integer } }
        X-RateLimit-Remaining: { schema: { type: integer } }
        Retry-After:           { schema: { type: integer, description: seconds } }
      content: { application/json: { schema: { $ref: '#/components/schemas/Problem' } } }
    ServiceUnavailable:
      description: Service unavailable — SDKs MUST fail open
      content: { application/json: { schema: { $ref: '#/components/schemas/Problem' } } }
