openapi: "3.1.0"
info:
  title: ShieldLabs API
  version: "1.2"
  description: |
    The ShieldLabs API surfaces visitor identification, anonymity detection, and
    abuse and fraud prevention for your application.

    There are three surfaces:

    - The **JS snippet** posts collected signals to `rest.shieldlabs.ai` automatically.
      You do not call this endpoint yourself. See [Install the snippet](/setup/snippet).
    - **Webhooks** deliver the Risk Score and signals to your server shortly after a
      visit. See [Webhooks](/api/webhooks). Contract: schema_version `2026-06-01`
      (`identification.scored` / `webhook.ping`), signature in `X-Shield-Signature`.
    - The **Server API** has two backend hosts:
      - **History API** (recommended) on `account.shieldlabs.ai/api` — implemented by
        **Shield.Portal.Admin**. Private API Key (`Authorization: Bearer sec_…`).
        Envelope `{ data, total }`. See [Server API](/api/server-api).
      - **Management API** on `api.shieldlabs.ai` — implemented by **Shield.Core**.
        Secret Key in headers (`X-Shield-Domain` + `Authorization: Bearer`).
        Profile, balance, and alternate billed History path (PascalCase array).

    The Risk Score is an integer from 0 to 100. ShieldLabs scores visits; your own
    code decides whether to allow, challenge, review, or block.
  contact:
    email: support@shieldlabs.ai

servers:
  - url: https://account.shieldlabs.ai/api
    description: History API (Shield.Portal.Admin — Private API Key, recommended for snapshot reads)
  - url: https://api.shieldlabs.ai
    description: Management API (Shield.Core — Secret Key — profile, balance, billed History)

tags:
  - name: history-api
    description: |
      Recommended snapshot reads on account.shieldlabs.ai (service: Shield.Portal.Admin).
      Authenticated with Private API Key. Response envelope `{ data, total }`.
  - name: management-api
    description: |
      Profile, balance, and alternate History on api.shieldlabs.ai (service: Shield.Core).
      Authenticated with Secret Key headers. History returns a PascalCase JSON array.

paths:
  /api/v1/history/{search_type}/{value}:
    get:
      tags: [history-api]
      summary: Search history by identifier (recommended)
      description: |
        Returns stored snapshots that match an identifier, newest first, in a paginated
        `{ data, total }` envelope. Does not consume request balance.
        Served by Shield.Portal.Admin on account.shieldlabs.ai.
      operationId: searchHistoryAccount
      security:
        - PrivateApiKey: []
      parameters:
        - name: search_type
          in: path
          required: true
          schema:
            type: string
            enum: [ip, user_hid, visitor_id, request_id, device_id, session_id, cookie_id]
        - name: value
          in: path
          required: true
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: offset
          in: query
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        "200":
          description: Paginated snapshot list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HistoryEnvelope"
        "401":
          $ref: "#/components/responses/UnauthorizedJson"

  /v1/profile:
    get:
      tags: [management-api]
      summary: Get domain profile
      description: |
        Returns the profile for a domain: remaining request balance and masked keys. Free (0 requests).
      operationId: getProfileV1
      security:
        - SecretKeyHeader: []
      responses:
        "200":
          description: Domain profile
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Profile"
        "401":
          $ref: "#/components/responses/UnauthorizedEmpty"

  /v1/history/{type}/{value}:
    get:
      tags: [management-api]
      summary: Search history by identifier (Management API)
      description: |
        Returns stored snapshots in a PascalCase JSON array, newest first. Each returned
        row consumes one request from your balance (an empty result still consumes one).
        Prefer `/api/v1/history/…` on account.shieldlabs.ai for new integrations.
        Served by Shield.Core on api.shieldlabs.ai.
      operationId: searchHistoryV1
      security:
        - SecretKeyHeader: []
      parameters:
        - name: type
          in: path
          required: true
          schema:
            type: string
            enum: [ip, user_hid, visitor_id, request_id, device_id, session_id, cookie_id]
        - name: value
          in: path
          required: true
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 100
      responses:
        "200":
          description: Matching snapshots, newest first
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Snapshot"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/UnauthorizedEmpty"
        "402":
          $ref: "#/components/responses/OutOfRequests"
        "404":
          description: Unsupported identifier type
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

webhooks:
  identificationScored:
    post:
      summary: identification.scored delivered
      description: |
        ShieldLabs POSTs a signed JSON envelope to your configured webhook endpoint after
        a visit is scored. One delivery per check (at-most-once, no retries).

        Verify `X-Shield-Signature: sha256=<hex>` (HMAC-SHA256 over the raw body with your
        endpoint secret). Treat `data.request_id` as an idempotency key.

        Schema version is `2026-06-01` (`schema_version` field). Ping/test deliveries use
        `event_type: webhook.ping` with no `data`.
      parameters:
        - name: X-Shield-Signature
          in: header
          required: true
          description: |
            HMAC-SHA256 of the raw request body, hex-encoded, prefixed with `sha256=`.
            Example: `sha256=9f1c2b3a…`.
          schema:
            type: string
        - name: Content-Type
          in: header
          required: true
          schema:
            type: string
            enum: [application/json]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebhookEvent"
            examples:
              scored:
                summary: identification.scored
                value:
                  event_type: identification.scored
                  schema_version: "2026-06-01"
                  created_at: "2026-06-26T14:20:42Z"
                  data:
                    request_id: "13f84f05-7c2a-4e9b-9f1d-2a6b8c0e4d11"
                    visitor_id: "161dfbad-8e7f-4a6b-9c5d-0e1f2a3b4c5d"
                    device_id: "5eb7fd5c-2a1b-4c3d-9e8f-7a6b5c4d3e2f"
                    session_id: "7a1b2c3d-4e5f-6789-abcd-ef0123456789"
                    cookie_id: "3f2e1d0c-9b8a-7654-3210-fedcba987654"
                    user_hid: null
                    domain: example.com
                    public_ip: { ip: "203.0.113.42", country: "US" }
                    local_ip: { ip: "198.51.100.23", country: "DE" }
                    connection_type: proxy
                    os: Windows
                    browser: Chrome
                    device_type: desktop
                    traffic_source:
                      channel: "Google Ads"
                      referrer_domain: google.com
                      landing_url: "https://example.com/lp?gclid=abc123"
                      click_id_type: gclid
                      utm_source: google
                      utm_medium: cpc
                      utm_campaign: summer_sale
                      utm_content: ad_a
                      utm_term: "buy shoes"
                    risk_score: 30
                    signals:
                      - { name: proxy, weight: 10 }
                      - { name: datacenter_ip, weight: 10 }
                      - { name: abuser, weight: 10 }
                    detection_flags:
                      vpn: false
                      privacy_relay: false
                      browser_vpn_proxy: false
                      tor: false
                      proxy: true
                      datacenter_ip: true
                      abuser: true
                      os_mismatch: false
                      os_not_detected: false
                      timezone_mismatch: false
                      anti_detect_browser: false
                      browser_automation: false
                      ip_mismatch: true
                      incognito: false
                      search_bot: false
                      suspicious_paid_click: false
                      javascript_disabled: false
                      stun_not_checked: false
                    observed_at: "2026-06-26T14:20:42Z"
              ping:
                summary: webhook.ping
                value:
                  event_type: webhook.ping
                  schema_version: "2026-06-01"
                  created_at: "2026-06-26T14:20:42Z"
      responses:
        "200":
          description: Acknowledged. Return 200 quickly.

components:
  securitySchemes:
    PrivateApiKey:
      type: http
      scheme: bearer
      description: Private API Key from the dashboard API tab (`sec_…`).
    SecretKeyHeader:
      type: http
      scheme: bearer
      description: Secret Key plus `X-Shield-Domain` header with your registered domain.

  responses:
    UnauthorizedEmpty:
      description: Missing or invalid credentials, or the domain is disabled (empty body)
    UnauthorizedJson:
      description: Missing or invalid Private API Key
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    BadRequest:
      description: Invalid parameters (bare JSON string on Management API)
    OutOfRequests:
      description: The domain has no remaining request balance (empty body)
    TooManyRequests:
      description: Per-IP rate limit exceeded
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    ServiceUnavailable:
      description: Gateway concurrency cap reached
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

  schemas:
    Error:
      type: object
      properties:
        error:
          type: string
          examples: ["too many requests", "invalid api key"]

    HistoryEnvelope:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/AccountSnapshot"
        total:
          type: integer

    AccountSnapshot:
      type: object
      description: Snapshot row from the History API (snake_case).
      properties:
        request_id:
          type: string
          format: uuid
        session_id:
          type: string
          format: uuid
        cookie_id:
          type: string
          format: uuid
        device_id:
          type: string
          format: uuid
        visitor_id:
          type: string
          format: uuid
        ip:
          type: string
        score:
          type: integer
        score_details:
          type: string
          description: JSON string of signal objects with Value and Description fields.
        created_at:
          type: string

    ScoreDetail:
      type: object
      description: Signal row on Management API Snapshot.Details (PascalCase). Not used in webhooks.
      properties:
        Value:
          type: integer
        Description:
          type: string

    WebhookIPAddress:
      type: object
      properties:
        ip:
          type: string
        country:
          type: string

    WebhookSignal:
      type: object
      required: [name, weight]
      properties:
        name:
          type: string
          description: Stable signal slug (e.g. proxy, vpn, datacenter_ip).
        weight:
          type: integer
          description: Points contributed to risk_score. Debug entries with weight 0 are omitted.

    WebhookTrafficSource:
      type: object
      properties:
        channel:
          type: string
        referrer_domain:
          type: string
        landing_url:
          type: string
        click_id_type:
          type: string
        utm_source:
          type: string
        utm_medium:
          type: string
        utm_campaign:
          type: string
        utm_content:
          type: string
        utm_term:
          type: string

    WebhookDetectionFlags:
      type: object
      description: Denormalized detection booleans from Shield.Core entity.WebhookDetectionFlags.
      properties:
        vpn:
          type: boolean
        privacy_relay:
          type: boolean
        browser_vpn_proxy:
          type: boolean
        tor:
          type: boolean
        proxy:
          type: boolean
        datacenter_ip:
          type: boolean
        abuser:
          type: boolean
        os_mismatch:
          type: boolean
        os_not_detected:
          type: boolean
        timezone_mismatch:
          type: boolean
        anti_detect_browser:
          type: boolean
        browser_automation:
          type: boolean
        ip_mismatch:
          type: boolean
        incognito:
          type: boolean
        search_bot:
          type: boolean
        suspicious_paid_click:
          type: boolean
        javascript_disabled:
          type: boolean
        stun_not_checked:
          type: boolean

    WebhookScoredData:
      type: object
      description: Payload inside identification.scored (Shield.Core entity.WebhookScoredData).
      required:
        - request_id
        - visitor_id
        - device_id
        - risk_score
        - signals
        - detection_flags
        - observed_at
      properties:
        request_id:
          type: string
          format: uuid
          description: Unique per visit. Use as an idempotency key.
        visitor_id:
          type: string
          format: uuid
        device_id:
          type: string
          format: uuid
        session_id:
          type: string
          format: uuid
        cookie_id:
          type: string
          format: uuid
        user_hid:
          type: ["string", "null"]
          description: Your hashed user identifier, if provided; null when empty.
        domain:
          type: string
        public_ip:
          $ref: "#/components/schemas/WebhookIPAddress"
        local_ip:
          $ref: "#/components/schemas/WebhookIPAddress"
        connection_type:
          type: string
          description: Observed connection class; new values may appear.
          enum: [direct, mobile, vpn, proxy, tor, privacy_relay, browser_vpn_proxy, unknown]
        os:
          type: string
        browser:
          type: string
        device_type:
          type: string
        traffic_source:
          $ref: "#/components/schemas/WebhookTrafficSource"
        risk_score:
          type: integer
          minimum: 0
          maximum: 100
        signals:
          type: array
          items:
            $ref: "#/components/schemas/WebhookSignal"
        detection_flags:
          $ref: "#/components/schemas/WebhookDetectionFlags"
        observed_at:
          type: string
          format: date-time

    WebhookEvent:
      type: object
      description: |
        POST body envelope (Shield.Core entity.WebhookEvent). Signature is NOT in the body —
        verify the X-Shield-Signature header.
      required: [event_type, schema_version, created_at]
      properties:
        event_type:
          type: string
          enum: [identification.scored, webhook.ping]
        schema_version:
          type: string
          const: "2026-06-01"
        created_at:
          type: string
          format: date-time
        data:
          $ref: "#/components/schemas/WebhookScoredData"
          description: Present for identification.scored; omitted for webhook.ping.

    Snapshot:
      type: object
      description: Snapshot from the Management API History path (PascalCase array element).
      properties:
        RequestID:
          type: string
          format: uuid
        SessionID:
          type: string
          format: uuid
        CookieID:
          type: string
          format: uuid
        DeviceID:
          type: string
          format: uuid
        VisitorID:
          type: string
          format: uuid
        IP:
          type: string
        ConnectionType:
          type: string
          enum: [direct, mobile, vpn, proxy, tor, privacy_relay, unknown]
        WebRtcHIP:
          type: string
        OS:
          type: string
        Browser:
          type: string
        DeviceType:
          type: string
        Country:
          type: string
        UserHID:
          type: string
        Score:
          type: integer
        Details:
          type: array
          items:
            $ref: "#/components/schemas/ScoreDetail"
        LastRequestTime:
          type: string
          format: date-time

    Profile:
      type: object
      properties:
        Domain:
          type: string
        Weight:
          type: integer
        PublicKey:
          type: string
        Secret:
          type: string
        CreatedAt:
          type: string
          format: date-time
