openapi: 3.1.0
info:
  title: Syncra Scoring API (KA surface)
  version: 1.0.0
  summary: Client scoring and checkout verdicts for external platform integrators.
  description: |
    The scoring contour of Syncra: pre-payment client scoring
    (`/client/score`), checkout verdicts with the cooldown ladder
    (`/client/verdict`) and, for integrators provisioned with the
    corresponding scope, checkout anti-bot gates and payment lifecycle
    reporting.

    **Every element of this specification is live-verified** against the
    stage environment with a scoped partner key unless explicitly marked
    `deferred` (each `deferred` carries a justification and a TD link).

    Wire contract highlights:
    * All requests and responses are `application/json` (charset UTF-8).
    * Authentication: the `X-KA-Access-Key` header, matched by the service
      in constant time. A key carries its platform identity and a scope
      set; endpoints outside the provisioned scope answer `403`.
    * Errors: through this publish point the edge re-wraps the KA error
      body into RFC 9457 `problem+json` — the KA text rides the `detail`
      field verbatim (see the `ErrorEnvelope` schema). Match on the HTTP
      status, never on message wording.
    * Bodies are limited to 16 KiB.
    * Amounts are decimal STRINGS (`"1500.00"`), never JSON numbers; a
      JSON number in an amount field is a `400`.
    * Rate limits: the shared unauthenticated gateway bucket
      (20 req/s per source IP) applies at the publish point; domain
      limits surface as semantic refusals (see per-endpoint `reason`
      enumerations), never as HTTP `429`.
  contact:
    name: Syncra Platform Owner
    url: https://syncra.money
  license:
    name: Proprietary
    identifier: LicenseRef-Proprietary
servers:
  - url: https://api-stage.syncra.money/api/v1/scoring
    description: Stage (integration testing — use the stage key)
  - url: https://api.syncra.money/api/v1/scoring
    description: Production (when provisioned by the platform owner)
security:
  - accessKey: []
tags:
  - name: scoring
    description: Pre-payment client scoring. Scope `scoring`.
  - name: checkout
    description: Checkout anti-bot gates. Scope `checkout`.
  - name: payments
    description: Payment lifecycle reporting. Scope `payments`.
paths:
  /v1/client/score:
    post:
      tags: [scoring]
      operationId: scoreClient
      summary: Score a client before showing payment options
      description: |
        Returns the client group, the empirical payment score and the bot
        flag. Idempotent by the `(sellPointKey, clientIdKey)` identity:
        repeating the same request simply re-scores the same client.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ScoreRequest"
            examples:
              typical:
                summary: First-time client, 1500 RUB basket
                value:
                  sellPointKey: legacy-shop-ru-1
                  clientIdKey: f2b1b3e4-client-4711
                  amountRequested: "1500.00"
                  toPayCurrency: RUB
                  selectedMethodKey: card_ru
                  fingerprint: 9f8e2ab41c
                  ip: 203.0.113.7
                  userAgent: LegacySyncra/2.4 (Windows NT 10.0; Win64; x64)
      responses:
        "200":
          description: The scoring verdict for this client.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ScoreResponse"
              examples:
                newClient:
                  summary: New client — neutral score, not a bot
                  value:
                    clientGroup: NEW
                    paymentScore: "0.5000"
                    isBot: false
                trustedClient:
                  summary: Trusted client — high score
                  value:
                    clientGroup: TRUSTED
                    paymentScore: "0.9812"
                    isBot: false
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/Internal"
  /v1/client/verdict:
    post:
      tags: [scoring]
      operationId: clientVerdict
      summary: Ask whether a checkout may proceed now
      description: |
        The cooldown-ladder verdict: `GET_SAME_STICKY` keeps the client on
        the previously bound requisite, `GET_NEW` issues a fresh one,
        `WAIT` asks the integrator to wait `waitSeconds` before retrying,
        `BLOCK` refuses the checkout. `FAIL_OPEN` never crosses the wire.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/VerdictRequest"
            examples:
              typical:
                summary: Second requisite request within a minute
                value:
                  sellPointKey: legacy-shop-ru-1
                  clientIdKey: f2b1b3e4-client-4711
                  method: card_ru
                  amountRequested: "1500.00"
                  toPayCurrency: RUB
                  orderRef: legacy-order-100500
      responses:
        "200":
          description: The verdict.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VerdictResponse"
              examples:
                getNew:
                  summary: Fresh requisite allowed (live-verified response)
                  value:
                    verdict: GET_NEW
                    reason: ladder level 1 granted
                    ruleVersion: p2.v10
                    isBot: false
                wait:
                  summary: Cooldown ladder — wait 15 seconds
                  value:
                    verdict: WAIT
                    waitSeconds: 15
                    reason: ladder level 1 wait
                    ruleVersion: p2.v10
                    isBot: false
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/Internal"
components:
  securitySchemes:
    accessKey:
      type: apiKey
      in: header
      name: X-KA-Access-Key
      description: |
        The scoped partner key provisioned by the platform owner
        (`name:key:scope[+scope...]` on the service side). The example
        value is a PLACEHOLDER — the real key never appears in this file.
  responses:
    BadRequest:
      description: Malformed JSON or a field failing validation.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
          examples:
            amountAsNumber:
              summary: Amount sent as a JSON number
              value:
                title: Bad Request
                status: 400
                detail: '{"message":"malformed JSON body"}'
    Unauthorized:
      description: Missing or unknown access key.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
          examples:
            missingKey:
              value:
                title: Unauthorized
                status: 401
                detail: '{"message":"X-KA-Access-Key header is required"}'
            unknownKey:
              value:
                title: Unauthorized
                status: 401
                detail: '{"message":"unknown X-KA-Access-Key"}'
    Forbidden:
      description: The key is valid but not provisioned for this endpoint's scope.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
          examples:
            scopeRefused:
              value:
                title: Forbidden
                status: 403
                detail: '{"message":"X-KA-Access-Key is not provisioned for this endpoint (scope payments)"}'
    Internal:
      description: Unexpected server error (details only in server logs).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
          examples:
            internal:
              value:
                title: Internal Server Error
                status: 500
                detail: '{"message":"internal error"}'
  schemas:
    ErrorEnvelope:
      type: object
      description: >-
        The edge re-wraps the KA error body into RFC 9457 problem+json;
        the KA `{"message"}` text rides `detail` verbatim.
      required: [title, status, detail]
      properties:
        type:
          type: string
        title:
          type: string
        status:
          type: integer
        detail:
          type: string
          description: >-
            The KA wire body verbatim — a JSON string containing the
            {"message": "..."} text.
        instance:
          type: string
      additionalProperties: false
    ScoreRequest:
      type: object
      required: [sellPointKey, clientIdKey, amountRequested, selectedMethodKey]
      properties:
        sellPointKey:
          $ref: "#/components/schemas/SellPointKey"
        clientIdKey:
          $ref: "#/components/schemas/ClientIdKey"
        amountRequested:
          $ref: "#/components/schemas/AmountDecimal"
        toPayCurrency:
          type: string
          minLength: 3
          maxLength: 4
          description: ISO 4217 alpha code (`RUB`, `TRY`). Optional; the service falls back to its configured currency.
          examples: [RUB]
        selectedMethodKey:
          $ref: "#/components/schemas/MethodKey"
        fingerprint:
          $ref: "#/components/schemas/Fingerprint"
        ip:
          $ref: "#/components/schemas/ClientIp"
        userAgent:
          $ref: "#/components/schemas/UserAgent"
        clientHints:
          type: object
          properties:
            jsOk:
              type: boolean
              description: The client page executed JavaScript.
            honeypotTriggered:
              type: boolean
              description: The hidden honeypot field was filled (a bot signal).
          additionalProperties: false
      additionalProperties: false
    ScoreResponse:
      type: object
      required: [clientGroup, paymentScore, isBot]
      properties:
        clientGroup:
          type: string
          enum: [NEW, STICKY, PAYING, TRUSTED, BLOCKED]
          description: |
            The client segment: `NEW` — first contact; `STICKY` — bound to
            a requisite; `PAYING` — paid before; `TRUSTED` — sustained
            payment history; `BLOCKED` — blocked by a manual decision or an
            unpaid chain.
        paymentScore:
          type: string
          description: Empirical payment probability, 4 decimal places (`0.5000` for a new client — Laplace-smoothed).
          examples: ["0.5000", "0.9812"]
        preferredAcceptorKey:
          type: array
          items:
            type: string
          description: Present only for sticky clients — the acceptor(s) the client is bound to.
        payLimitValue:
          description: >-
            Present only when the platform enforces score-based pay limits
            — a JSON array of rows like {"limit": "1500.00"}.
          type: array
          items:
            type: object
            properties:
              limit:
                type: string
            additionalProperties: true
        isBot:
          type: boolean
          description: The bot flag from the anti-fraud fold (velocity, fingerprint sharing, honeypot…).
      additionalProperties: false
    VerdictRequest:
      type: object
      required: [sellPointKey, clientIdKey, method, amountRequested, orderRef]
      properties:
        sellPointKey:
          $ref: "#/components/schemas/SellPointKey"
        clientIdKey:
          $ref: "#/components/schemas/ClientIdKey"
        method:
          $ref: "#/components/schemas/MethodKey"
        amountRequested:
          $ref: "#/components/schemas/AmountDecimal"
        toPayCurrency:
          type: string
          minLength: 3
          maxLength: 4
        orderRef:
          type: string
          minLength: 1
          maxLength: 256
          description: The integrator's order identifier (opaque to the platform).
      additionalProperties: false
    VerdictResponse:
      type: object
      required: [verdict, reason, ruleVersion, isBot]
      properties:
        verdict:
          type: string
          enum: [GET_SAME_STICKY, GET_NEW, WAIT, BLOCK]
        stickyRequisiteRef:
          type: string
          description: Present with `GET_SAME_STICKY` — the requisite reference to reuse.
        waitSeconds:
          type: integer
          format: int64
          minimum: 0
          description: Present with `WAIT` — how long to wait before retrying (cooldown ladder levels).
        reason:
          type: string
          description: >-
            Human-readable rule outcome (free-form; may change between rule
            versions — match on `verdict`, not on `reason`).
          examples: [ok, ladder level 1 granted]
        ruleVersion:
          type: string
          description: The rule-set version that produced the verdict.
          examples: [p2.v10]
        isBot:
          type: boolean
      additionalProperties: false
    SellPointKey:
      type: string
      minLength: 1
      maxLength: 256
      description: The integrator's selling-point identifier (stable per checkout origin).
      examples: [legacy-shop-ru-1]
    ClientIdKey:
      type: string
      minLength: 1
      maxLength: 256
      description: The integrator's stable client identifier (the scoring memory key).
      examples: [f2b1b3e4-client-4711]
    MethodKey:
      type: string
      minLength: 1
      maxLength: 256
      description: The payment method selector agreed with the platform owner.
      examples: [card_ru, sbp_rub, iban_try]
    AmountDecimal:
      type: string
      pattern: '^\d+(\.\d+)?$'
      description: |
        Positive decimal amount as a STRING. Sending a JSON number is a
        `400`.
      examples: ["1500.00"]
    Fingerprint:
      type: string
      maxLength: 256
      description: A stable browser/device fingerprint token the integrator can compute.
      examples: [9f8e2ab41c]
    ClientIp:
      type: string
      maxLength: 64
      description: The client's IP as seen by the integrator (used for velocity and shared-IP signals).
      examples: [203.0.113.7]
    UserAgent:
      type: string
      maxLength: 512
      description: The client's User-Agent header value.
      examples: ["LegacySyncra/2.4 (Windows NT 10.0; Win64; x64)"]
