openapi: 3.1.0
info:
  title: FingerTrackID Feedback and Assessment Delivery
  version: 1.0.0
  description: |
    Versioned server-to-server feedback wire and complementary assessment delivery.
    Private development/staging runtime source exists; behavioral validation is pending.
    No public runtime endpoint or outbound sender is enabled.
    The independent verifier authenticates the original request, commits replay
    consumption, applies the shared limiter, parses the closed body and derives the
    lossless effect before authorization. The feedback owner derives the tenant,
    application, integration client, actor and business event from trusted state.
    The edge never receives a feedback database capability or an inbound secret.

    Only acknowledged fact + receipt + audit COMMIT permits success: 201 for NEW,
    200 for SAME, and 409 for divergent reuse. A timeout or lost COMMIT acknowledgement
    is indeterminate, not proof that no effect exists. Retry preserves the exact
    logical request and idempotency key, using a NEW request ID, timestamp and HMAC.
    No response executes a customer action or promotes an outcome to a fraud label.

    This document includes JSON Schema components, but their standard keywords are
    not the entire raw-input profile. The x-ftid-raw-input-profile and UTF-16 length
    checks below are normative and must be implemented by every request validator.
    Standalone schemas are generated from these components. Independent parity and
    raw-profile validation remain release gates; generation is not validation.
  license:
    name: Proprietary
servers:
  - url: https://api.fingertrackid.dev
    description: Target endpoint; publication is not operational availability
x-implementation-status: private-runtime-implemented-validation-pending
x-ftid-raw-input-profile:
  maximum_body_bytes: 16384
  encoding: UTF-8_WITHOUT_BOM
  root: ONE_CLOSED_FLAT_OBJECT
  reject: [DUPLICATE_DECODED_MEMBERS, UNKNOWN_MEMBERS, NESTED_VALUES, NUL, UNPAIRED_SURROGATES, TRAILING_DOCUMENT]
  content_type: application/json
  content_encoding: FORBIDDEN_INCLUDING_IDENTITY
  request_target: EXACT_ORIGIN_FORM_NO_QUERY_NO_NORMALIZATION
  numeric_confidence: |
    A number must equal the exact decimal value of its parsed binary64 Number's
    shortest roundtrip decimal spelling. Equivalent exponent/trailing-zero forms
    are allowed; silent rounding and underflow are rejected. Positive and negative
    zero are one semantic value. The effect uses binary64 big-endian hex, not a
    rounded SQL numeric projection.
  lossless_effect: ../canonicalization/feedback-effect-v1.md
  raw_parser: ../../packages/contracts/src/feedback-raw-input-v1.ts
x-ftid-recovery-gates:
  supported_contract: SAME_POST_WITH_NEW_HMAC_ATTEMPT
  assessment_id_get: |
    Disabled. GET /v1/assessments/{assessment_id} would use an empty raw body,
    Content-Type application/json, no Content-Encoding/query, and matching
    X-FTID-Assessment-Id. Each read must have fresh HMAC/replay admission and
    server-derived scope. ADR-020 does not yet define a read capability or read
    authorization owner. Do not borrow assessment:write or expose this route.
  external_event_id: |
    Recover with the existing identical POST /v1/assessments contract. A standalone
    lookup requires an explicit authority and retention contract before publication.
  receipt_id: |
    A domain receipt is a correlation handle, not bearer authority or an authorization
    receipt. No public unauthenticated receipt lookup is defined.
  correlation_id: |
    Support correlation only, never a tenant selector, capability or public read key.
  retention: |
    A tombstone does not promise full-result retention. EXT-003 must close the
    retrieval lifetime and post-erasure representation before read activation.
x-ftid-signature-vector:
  {
    "schema_version": "assessment-webhook-signature-vector.v1",
    "status": "FIXTURE_GENERATED_NOT_INDEPENDENTLY_VALIDATED",
    "protocol": "FTID-HMAC-SHA256-V1",
    "secret_hex_public_synthetic_only": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
    "input": {
      "key_id": "01996f2a-4444-7000-8000-000000000004",
      "method": "POST",
      "path": "/ftid/assessment-deliveries",
      "timestamp": 1787616000,
      "assessment_id": "01996f2a-0000-7000-8000-000000000401",
      "delivery_id": "01996f2a-5555-7000-8000-000000000005",
      "content_type": "application/json",
      "content_encoding": "",
      "raw_body_utf8": "{\"schema_version\":\"event-assessment.v1\",\"assessment_id\":\"01996f2a-0000-7000-8000-000000000401\",\"assessment_version\":1,\"supersedes_assessment_id\":null,\"business_event_id\":\"01996f2a-0000-7000-8000-000000000402\",\"external_event_id\":\"login_20260904_000002\",\"journey\":\"LOGIN\",\"assessment_status\":\"INCONCLUSIVE\",\"retryable\":false,\"trust_score\":null,\"device_intelligence_snapshot_id\":null,\"device_intelligence_snapshot_version\":null,\"device_id\":null,\"device_match_confidence\":null,\"evidence_coverage\":0,\"data_freshness\":{\"evidence_cutoff_at\":\"2026-09-04T12:00:00.105Z\",\"oldest_required_observed_at\":null,\"maximum_required_age_ms\":300000,\"all_required_fresh\":false},\"signals\":[{\"signal_code\":\"ANTI_FINGERPRINTING\",\"channel\":\"WEB\",\"signal_state\":\"NOT_EVALUATED\",\"applicability\":\"APPLICABLE\",\"source\":\"DERIVED\",\"detector_version\":\"unavailable-web-detectors.v1\",\"detector_confidence\":null,\"observed_at\":null,\"evaluated_at\":\"2026-09-04T12:00:00.143Z\",\"freshness_ms\":null,\"not_evaluated_reason_code\":\"COLLECTOR_NOT_LOADED\",\"applicability_reason_code\":null},{\"signal_code\":\"BOT_AUTOMATION\",\"channel\":\"WEB\",\"signal_state\":\"NOT_EVALUATED\",\"applicability\":\"APPLICABLE\",\"source\":\"DERIVED\",\"detector_version\":\"unavailable-web-detectors.v1\",\"detector_confidence\":null,\"observed_at\":null,\"evaluated_at\":\"2026-09-04T12:00:00.143Z\",\"freshness_ms\":null,\"not_evaluated_reason_code\":\"COLLECTOR_NOT_LOADED\",\"applicability_reason_code\":null},{\"signal_code\":\"BROWSER_PRIVACY_BRAVE\",\"channel\":\"WEB\",\"signal_state\":\"NOT_EVALUATED\",\"applicability\":\"APPLICABLE\",\"source\":\"DERIVED\",\"detector_version\":\"unavailable-web-detectors.v1\",\"detector_confidence\":null,\"observed_at\":null,\"evaluated_at\":\"2026-09-04T12:00:00.143Z\",\"freshness_ms\":null,\"not_evaluated_reason_code\":\"COLLECTOR_NOT_LOADED\",\"applicability_reason_code\":null},{\"signal_code\":\"BROWSER_PRIVACY_SAFARI\",\"channel\":\"WEB\",\"signal_state\":\"NOT_EVALUATED\",\"applicability\":\"APPLICABLE\",\"source\":\"DERIVED\",\"detector_version\":\"unavailable-web-detectors.v1\",\"detector_confidence\":null,\"observed_at\":null,\"evaluated_at\":\"2026-09-04T12:00:00.143Z\",\"freshness_ms\":null,\"not_evaluated_reason_code\":\"COLLECTOR_NOT_LOADED\",\"applicability_reason_code\":null},{\"signal_code\":\"BROWSER_PRIVACY_SAMSUNG_INTERNET\",\"channel\":\"WEB\",\"signal_state\":\"NOT_EVALUATED\",\"applicability\":\"APPLICABLE\",\"source\":\"DERIVED\",\"detector_version\":\"unavailable-web-detectors.v1\",\"detector_confidence\":null,\"observed_at\":null,\"evaluated_at\":\"2026-09-04T12:00:00.143Z\",\"freshness_ms\":null,\"not_evaluated_reason_code\":\"COLLECTOR_NOT_LOADED\",\"applicability_reason_code\":null},{\"signal_code\":\"BROWSER_SPOOFING\",\"channel\":\"WEB\",\"signal_state\":\"NOT_EVALUATED\",\"applicability\":\"APPLICABLE\",\"source\":\"DERIVED\",\"detector_version\":\"unavailable-web-detectors.v1\",\"detector_confidence\":null,\"observed_at\":null,\"evaluated_at\":\"2026-09-04T12:00:00.143Z\",\"freshness_ms\":null,\"not_evaluated_reason_code\":\"COLLECTOR_NOT_LOADED\",\"applicability_reason_code\":null},{\"signal_code\":\"DEVICE_MULTI_USER\",\"channel\":\"WEB\",\"signal_state\":\"NOT_EVALUATED\",\"applicability\":\"APPLICABLE\",\"source\":\"DERIVED\",\"detector_version\":\"unavailable-web-detectors.v1\",\"detector_confidence\":null,\"observed_at\":null,\"evaluated_at\":\"2026-09-04T12:00:00.143Z\",\"freshness_ms\":null,\"not_evaluated_reason_code\":\"COLLECTOR_NOT_LOADED\",\"applicability_reason_code\":null},{\"signal_code\":\"EMULATED_ENVIRONMENT\",\"channel\":\"WEB\",\"signal_state\":\"NOT_EVALUATED\",\"applicability\":\"APPLICABLE\",\"source\":\"DERIVED\",\"detector_version\":\"unavailable-web-detectors.v1\",\"detector_confidence\":null,\"observed_at\":null,\"evaluated_at\":\"2026-09-04T12:00:00.143Z\",\"freshness_ms\":null,\"not_evaluated_reason_code\":\"COLLECTOR_NOT_LOADED\",\"applicability_reason_code\":null},{\"signal_code\":\"NETWORK_HOSTING\",\"channel\":\"WEB\",\"signal_state\":\"NOT_EVALUATED\",\"applicability\":\"APPLICABLE\",\"source\":\"DERIVED\",\"detector_version\":\"unavailable-web-detectors.v1\",\"detector_confidence\":null,\"observed_at\":null,\"evaluated_at\":\"2026-09-04T12:00:00.143Z\",\"freshness_ms\":null,\"not_evaluated_reason_code\":\"COLLECTOR_NOT_LOADED\",\"applicability_reason_code\":null},{\"signal_code\":\"NETWORK_PROXY\",\"channel\":\"WEB\",\"signal_state\":\"NOT_EVALUATED\",\"applicability\":\"APPLICABLE\",\"source\":\"DERIVED\",\"detector_version\":\"unavailable-web-detectors.v1\",\"detector_confidence\":null,\"observed_at\":null,\"evaluated_at\":\"2026-09-04T12:00:00.143Z\",\"freshness_ms\":null,\"not_evaluated_reason_code\":\"COLLECTOR_NOT_LOADED\",\"applicability_reason_code\":null},{\"signal_code\":\"NETWORK_TOR\",\"channel\":\"WEB\",\"signal_state\":\"NOT_EVALUATED\",\"applicability\":\"APPLICABLE\",\"source\":\"DERIVED\",\"detector_version\":\"unavailable-web-detectors.v1\",\"detector_confidence\":null,\"observed_at\":null,\"evaluated_at\":\"2026-09-04T12:00:00.143Z\",\"freshness_ms\":null,\"not_evaluated_reason_code\":\"COLLECTOR_NOT_LOADED\",\"applicability_reason_code\":null},{\"signal_code\":\"NETWORK_VPN\",\"channel\":\"WEB\",\"signal_state\":\"NOT_EVALUATED\",\"applicability\":\"APPLICABLE\",\"source\":\"DERIVED\",\"detector_version\":\"unavailable-web-detectors.v1\",\"detector_confidence\":null,\"observed_at\":null,\"evaluated_at\":\"2026-09-04T12:00:00.143Z\",\"freshness_ms\":null,\"not_evaluated_reason_code\":\"COLLECTOR_NOT_LOADED\",\"applicability_reason_code\":null},{\"signal_code\":\"PRIVATE_BROWSING\",\"channel\":\"WEB\",\"signal_state\":\"NOT_EVALUATED\",\"applicability\":\"APPLICABLE\",\"source\":\"DERIVED\",\"detector_version\":\"unavailable-web-detectors.v1\",\"detector_confidence\":null,\"observed_at\":null,\"evaluated_at\":\"2026-09-04T12:00:00.143Z\",\"freshness_ms\":null,\"not_evaluated_reason_code\":\"COLLECTOR_NOT_LOADED\",\"applicability_reason_code\":null}],\"limitation_codes\":[\"COLLECTOR_NOT_LOADED\",\"REQUIRED_EVIDENCE_MISSING\"],\"reason_codes\":[\"REQUIRED_EVIDENCE_MISSING\"],\"recommended_action\":\"MONITOR\",\"recommendation_basis\":\"INSUFFICIENT_EVIDENCE\",\"policy_version\":\"production-detectors-01996f29-4d2c-7b18-9e6a-3f0c8d1b5a72-login\",\"fallback_policy_version\":\"production-observation-fallback-v2\",\"ruleset_version\":\"d95-detectors.v1\",\"signal_catalog_version\":\"web-detectors-v3\",\"feature_schema_version\":\"configured-assessment-features.v1\",\"resolver_version\":\"first-party-installation-v1\",\"sdk_version\":null,\"provider_versions\":[],\"evidence_cutoff_at\":\"2026-09-04T12:00:00.105Z\",\"evaluated_at\":\"2026-09-04T12:00:00.143Z\",\"expires_at\":\"2026-09-04T12:01:00.143Z\",\"correlation_id\":\"01996f2a-1111-7000-8000-000000000405\"}"
    },
    "expected": {
      "body_sha256": "cf0c74387739f220646b13543f69e63323f2e8e77fe265559042fcfc7a134c0f",
      "canonical_string": "FTID-HMAC-SHA256-V1\n01996f2a-4444-7000-8000-000000000004\nPOST\n/ftid/assessment-deliveries\n1787616000\nassessment_id:01996f2a-0000-7000-8000-000000000401\ndelivery_id:01996f2a-5555-7000-8000-000000000005\napplication/json\n\ncf0c74387739f220646b13543f69e63323f2e8e77fe265559042fcfc7a134c0f",
      "mac_hex": "619c6e89fb836f7ef642e3947189f3798f3e633fde460141efe29cb8afe539b5",
      "signature_header": "v1=01996f2a-4444-7000-8000-000000000004:619c6e89fb836f7ef642e3947189f3798f3e633fde460141efe29cb8afe539b5"
    }
  }
paths:
  /v1/assessments/{assessment_id}/customer-actions:
    post:
      operationId: recordCustomerActionV1
      summary: Record an action already executed by the customer
      x-implementation-status: planned
      x-ftid-operation-code: RECORD_CUSTOMER_ACTION
      x-ftid-required-capability: action:write
      x-ftid-max-request-body-bytes: 16384
      x-ftid-max-response-body-bytes: 65536
      security:
        - ftidHmacV1: []
      parameters:
        - $ref: '#/components/parameters/AssessmentId'
        - $ref: '#/components/parameters/FTIDAssessmentId'
        - $ref: './assessment.v1.yaml#/components/parameters/FTIDTimestamp'
        - $ref: './assessment.v1.yaml#/components/parameters/FTIDRequestId'
      description: |
        The signed assessment ID exactly equals the canonical path resource.
        Tenant/application/integration client/event/actor/alignment are forbidden
        in the body. The owner checks event.occurred_at <= action.occurred_at <=
        server_received_at + 300 seconds and derives alignment from the persisted
        recommendation: ALLOW/MONITOR -> ALLOWED, STEP_UP -> CHALLENGED,
        REVIEW -> REVIEWED or HELD, BLOCK_RECOMMENDED -> BLOCKED. Other pairs are
        NOT_ALIGNED; CANCELLED is NOT_COMPARABLE. ASSESSMENT basis requires ALIGNED.
        Matching the recommendation does not prove causal adherence or fraud.
        Target absence and a target outside the authenticated scope share 404.
        Feedback has separate capacity from online assessment; it is not a bulk API.
        Every response (201, 200 and every Problem) carries at most 65536 octets of
        content, inclusive, as on POST /v1/assessments; the server never sends a truncated one.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CustomerActionRequestV1' }
      responses:
        '201': { $ref: '#/components/responses/ActionCommitted' }
        '200': { $ref: '#/components/responses/ActionCommitted' }
        '400': { $ref: '#/components/responses/Problem' }
        '401': { $ref: '#/components/responses/AuthenticationProblem' }
        '403': { $ref: '#/components/responses/Problem' }
        '404': { $ref: '#/components/responses/Problem' }
        '409': { $ref: '#/components/responses/Problem' }
        '413': { $ref: '#/components/responses/Problem' }
        '415': { $ref: '#/components/responses/Problem' }
        '429': { $ref: '#/components/responses/RetryableProblem' }
        '500': { $ref: '#/components/responses/Problem' }
        '503': { $ref: '#/components/responses/RetryableProblem' }
  /v1/assessments/{assessment_id}/outcomes:
    post:
      operationId: appendCustomerOutcomeV1
      summary: Append an initial observation or a correction of the current leaf
      x-implementation-status: planned
      x-ftid-operation-code: APPEND_CUSTOMER_OUTCOME
      x-ftid-required-capability: outcome:write
      x-ftid-max-request-body-bytes: 16384
      x-ftid-max-response-body-bytes: 65536
      security:
        - ftidHmacV1: []
      parameters:
        - $ref: '#/components/parameters/AssessmentId'
        - $ref: '#/components/parameters/FTIDAssessmentId'
        - $ref: './assessment.v1.yaml#/components/parameters/FTIDTimestamp'
        - $ref: './assessment.v1.yaml#/components/parameters/FTIDRequestId'
      description: |
        Every nullable member is required. A single linear chain belongs to the
        server-derived tenant/business event/owning integration client, shared by
        all assessment revisions. The path must identify the original synchronous
        assessment; late revisions are rejected as ingestion targets and reference the
        same event-wide chain for subsequent reads. The path does not create a new chain.
        A root requires INITIAL_OBSERVATION and null predecessor; a successor must
        name the current leaf. A second root, stale ancestor or competing successor
        is an audited 409 binding-conflict. No row is overwritten.

        The owner checks event.occurred_at <= observed_at <= server_received_at +
        300 seconds, retaining the exact source timestamp and binary64 confidence
        identity. Source/fraud catalogs and actor qualification are resolved server-side.
        Correction precedence is LABEL_RETRACTED when a mature conclusive candidate
        ceases to qualify, otherwise STATUS_CORRECTED for changed truth, otherwise
        EVIDENCE_MATURED for unchanged truth and PROVISIONAL -> MATURE, otherwise
        EVIDENCE_CORRECTED for changed evidence/fraud type/observed_at while truth and
        maturity stay unchanged. A no-op or reason/diff mismatch is rejected.
        EVIDENCE_CORRECTED cannot hide MATURE -> PROVISIONAL.

        The receipt acknowledges this immutable fact, not that it remains the current
        leaf. Maturity/evidence submitted by the customer never imply ELIGIBLE;
        adjudication is separate and remains PENDING_VALIDATION until EXT-007.
        Target absence and a target outside the authenticated scope share 404.
        Every response (201, 200 and every Problem) carries at most 65536 octets of
        content, inclusive, as on POST /v1/assessments; the server never sends a truncated one.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CustomerOutcomeRequestV1' }
      responses:
        '201': { $ref: '#/components/responses/OutcomeCommitted' }
        '200': { $ref: '#/components/responses/OutcomeCommitted' }
        '400': { $ref: '#/components/responses/Problem' }
        '401': { $ref: '#/components/responses/AuthenticationProblem' }
        '403': { $ref: '#/components/responses/Problem' }
        '404': { $ref: '#/components/responses/Problem' }
        '409': { $ref: '#/components/responses/Problem' }
        '413': { $ref: '#/components/responses/Problem' }
        '415': { $ref: '#/components/responses/Problem' }
        '429': { $ref: '#/components/responses/RetryableProblem' }
        '500': { $ref: '#/components/responses/Problem' }
        '503': { $ref: '#/components/responses/RetryableProblem' }
webhooks:
  assessment:
    post:
      operationId: receiveAssessmentWebhookV1
      summary: Receive the exact existing immutable assessment
      x-implementation-status: planned
      x-ftid-max-request-body-bytes: 65536
      x-ftid-max-canonical-path-bytes: 1024
      security:
        - ftidOutboundHmacV1: []
      description: |
        POST goes only to an ownership-verified, active HTTPS:443 destination.
        The body is the stored assessment itself, in the form the synchronous answer
        had: event-assessment.v1 for an event-assessment-request.v1 call and
        event-assessment.v1.1 (customer tag instead of journey) for an
        event-assessment-request.v1.1 call. schema_version names the form; the two
        are never mixed. It is not a separately recalculated score or a wrapper that
        changes its meaning. Persist these exact body bytes once;
        retries keep them unchanged and use a new delivery_id/timestamp. The
        assessment_id is equal in body and signed header on every attempt.
        Deduplicate by assessment_id (or business_event_id + assessment_version),
        and independently reject replayed delivery_id. A new assessment revision is
        a distinct append-only resource, not an overwrite of an earlier response.

        Use FTID-HMAC-SHA256-V1, POST, the original configured path, and exactly
        assessment_id:<id> / delivery_id:<id> tags. No query/path normalization,
        redirects or compression are permitted. The endpoint path is ASCII and
        at most 1024 bytes. At most two signatures are present during rotation;
        key IDs are distinct and each identifies the exact outbound key version.
        Accept timestamps only within the inclusive +/-300-second window, compare
        MACs in constant time and commit the replay ledger before acceptance.
        Authentication and a 2xx receipt do not prove a customer action or fraud.

        Sender activation requires a canonical global provider-secret registry
        that rejects inbound/outbound resource reuse even through different aliases,
        endpoint-scoped outbound secrets, fixed approved IP with original Host/SNI,
        DNS revalidation, no redirects, bounded responses, durable outbox, replay,
        retry/DLQ and delivery outcome persistence. This schema does not implement them.
      parameters:
        - $ref: '#/components/parameters/FTIDAssessmentId'
        - $ref: './assessment.v1.yaml#/components/parameters/FTIDTimestamp'
        - name: X-FTID-Delivery-Id
          in: header
          required: true
          schema: { $ref: '#/components/schemas/UuidV7' }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - $ref: '../json-schema/event-assessment.v1.schema.json'
                - $ref: '../json-schema/event-assessment.v1.1.schema.json'
      responses:
        '200': { description: Receipt acknowledged; no customer action is inferred }
        '204': { description: Receipt acknowledged without a response body }
        default: { description: Non-2xx or indeterminate delivery; bounded retry policy applies }
components:
  securitySchemes:
    ftidHmacV1:
      type: apiKey
      in: header
      name: X-FTID-Signature
      description: |
        Exactly v1=<lowercase UUIDv7 key_id>:<64 lowercase hex HMAC>. Existing
        FTID-HMAC-SHA256-V1 signs key ID, method, exact path, timestamp,
        assessment_id:<path ID>, request_id:<attempt ID>, application/json, empty
        content-encoding line, and SHA-256 of the original body bytes, with LF
        separators and no final LF. No X-Client-Id, legacy alias or multiple inbound
        signatures. Known/unknown/invalid credentials share the frozen 401 response.
        See hmac-v1-test-vector.json for the already frozen customer-action vector.
    ftidOutboundHmacV1:
      type: apiKey
      in: header
      name: X-FTID-Signature
      description: |
        One or at most two comma-separated v1=<key_id>:<64 lowercase hex HMAC>
        signatures; no whitespace and no duplicate key IDs. The same canonical v1
        protocol uses delivery_id instead of request_id. Inbound secret material
        never signs an outbound delivery. Protocol shape does not prove key purpose.
  parameters:
    AssessmentId:
      name: assessment_id
      in: path
      required: true
      schema: { $ref: '#/components/schemas/UuidV7' }
    FTIDAssessmentId:
      name: X-FTID-Assessment-Id
      in: header
      required: true
      description: Exact canonical assessment ID, equal to the body for webhooks and path for feedback
      schema: { $ref: '#/components/schemas/UuidV7' }
  headers:
    CorrelationId:
      description: Server-generated attempt correlation; not read authority or a customer-selected tenant
      schema: { type: string, minLength: 1, maxLength: 128 }
    NoStore:
      schema: { type: string, const: no-store }
    RetryAfter:
      description: Bounded server-selected retry delay in whole seconds; retry signs a fresh attempt
      schema: { type: string, pattern: '^[1-9][0-9]*$' }
  responses:
    ActionCommitted:
      description: Immutable receipt after acknowledged COMMIT; 201 NEW or 200 SAME without another effect
      headers:
        X-Correlation-Id: { $ref: '#/components/headers/CorrelationId' }
        Cache-Control: { $ref: '#/components/headers/NoStore' }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/CustomerActionReceiptV1' }
    OutcomeCommitted:
      description: Immutable receipt after acknowledged COMMIT; 201 NEW or 200 SAME even if later corrected
      headers:
        X-Correlation-Id: { $ref: '#/components/headers/CorrelationId' }
        Cache-Control: { $ref: '#/components/headers/NoStore' }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/CustomerOutcomeReceiptV1' }
    Problem:
      description: Closed Problem; body status must equal HTTP status and the fixed type/status/retry matrix
      headers:
        X-Correlation-Id: { $ref: '#/components/headers/CorrelationId' }
        Cache-Control: { $ref: '#/components/headers/NoStore' }
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/FeedbackProblemV1' }
    AuthenticationProblem:
      description: Uniform 401, or retryable timestamp/replay rejection after valid MAC and durable nonce admission
      headers:
        WWW-Authenticate:
          required: true
          schema: { type: string, const: 'FTID-HMAC-V1 realm="assessments"' }
        X-Correlation-Id: { $ref: '#/components/headers/CorrelationId' }
        Cache-Control: { $ref: '#/components/headers/NoStore' }
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/FeedbackProblemV1' }
    RetryableProblem:
      description: No receipt is returned; state may be indeterminate, and a fresh signed attempt is required
      headers:
        Retry-After: { $ref: '#/components/headers/RetryAfter' }
        X-Correlation-Id: { $ref: '#/components/headers/CorrelationId' }
        Cache-Control: { $ref: '#/components/headers/NoStore' }
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/FeedbackProblemV1' }
  schemas:
    UuidV7:
      type: string
      pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'
    ExternalId:
      type: string
      minLength: 1
      maxLength: 255
      x-ftid-max-utf16-code-units: 255
      description: |
        Opaque 1..255 UTF-16 code units, NOT Unicode code points. Supplementary
        scalars count as two units. Preserve whitespace and exact spelling; reject
        NUL and unpaired surrogates. maxLength alone is insufficient for this bound.
    IdempotencyKey:
      type: string
      minLength: 8
      maxLength: 255
      pattern: '^[\x21-\x7e]+$'
    SourceInstant:
      type: string
      description: Lossless existing Instante grammar; do not normalize, truncate or add a stricter RFC date-time format
      pattern: '^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$'
    ServerInstant:
      allOf:
        - $ref: '#/components/schemas/SourceInstant'
        - pattern: 'T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]{1,3})?(?:Z|[+-][0-9]{2}:[0-9]{2})$'
    CatalogCode:
      type: string
      pattern: '^[A-Z][A-Z0-9_]{2,63}$'
    CustomerActionRequestV1:
      type: object
      additionalProperties: false
      required: [schema_version, external_action_id, idempotency_key, action, basis, occurred_at, reason_code]
      properties:
        schema_version: { const: customer-action.v1 }
        external_action_id: { $ref: '#/components/schemas/ExternalId' }
        idempotency_key: { $ref: '#/components/schemas/IdempotencyKey' }
        action: { enum: [ALLOWED, CHALLENGED, HELD, REVIEWED, BLOCKED, CANCELLED] }
        basis: { enum: [ASSESSMENT, CLIENT_POLICY, FALLBACK, MANUAL_REVIEW] }
        occurred_at: { $ref: '#/components/schemas/SourceInstant' }
        reason_code: { enum: [FOLLOWED_FTID_RECOMMENDATION, CLIENT_POLICY_RULE, CLIENT_FALLBACK_APPLIED, MANUAL_REVIEW_DECISION, EXTERNAL_FRAUD_CONTROL, CUSTOMER_CANCELLED] }
      allOf:
        - oneOf:
            - properties:
                basis: { const: ASSESSMENT }
                reason_code: { const: FOLLOWED_FTID_RECOMMENDATION }
            - properties:
                basis: { const: CLIENT_POLICY }
                reason_code: { enum: [CLIENT_POLICY_RULE, EXTERNAL_FRAUD_CONTROL, CUSTOMER_CANCELLED] }
            - properties:
                basis: { const: FALLBACK }
                reason_code: { const: CLIENT_FALLBACK_APPLIED }
            - properties:
                basis: { const: MANUAL_REVIEW }
                reason_code: { enum: [MANUAL_REVIEW_DECISION, CUSTOMER_CANCELLED] }
        - if:
            properties: { reason_code: { const: CUSTOMER_CANCELLED } }
          then:
            properties: { action: { const: CANCELLED } }
    CustomerOutcomeRequestV1:
      type: object
      additionalProperties: false
      required: [schema_version, external_outcome_id, idempotency_key, truth_status, label_maturity, fraud_type, evidence_source, evidence_reference, evidence_confidence, observed_at, reason_code, corrects_external_outcome_id]
      properties:
        schema_version: { const: customer-outcome.v1 }
        external_outcome_id: { $ref: '#/components/schemas/ExternalId' }
        idempotency_key: { $ref: '#/components/schemas/IdempotencyKey' }
        truth_status: { enum: [PENDING, INSUFFICIENT_EVIDENCE, LEGITIMATE, FRAUD_CONFIRMED] }
        label_maturity: { enum: [PROVISIONAL, MATURE] }
        fraud_type:
          anyOf: [{ $ref: '#/components/schemas/CatalogCode' }, { type: 'null' }]
        evidence_source:
          anyOf: [{ $ref: '#/components/schemas/CatalogCode' }, { type: 'null' }]
        evidence_reference:
          type: [string, 'null']
          x-ftid-nonempty-after-ecmascript-trim: true
          description: Null or a string nonempty after ECMAScript trim; preserve source and never fetch this reference as a URL
        evidence_confidence: { type: [number, 'null'], minimum: 0, maximum: 1 }
        observed_at: { $ref: '#/components/schemas/SourceInstant' }
        reason_code: { enum: [INITIAL_OBSERVATION, EVIDENCE_MATURED, STATUS_CORRECTED, EVIDENCE_CORRECTED, LABEL_RETRACTED] }
        corrects_external_outcome_id:
          anyOf: [{ $ref: '#/components/schemas/ExternalId' }, { type: 'null' }]
      x-ftid-cross-field: external_outcome_id must differ from non-null corrects_external_outcome_id
      allOf:
        - if:
            properties: { truth_status: { const: PENDING } }
          then:
            properties: { label_maturity: { const: PROVISIONAL } }
        - if:
            properties: { truth_status: { const: FRAUD_CONFIRMED } }
          then:
            properties: { fraud_type: { $ref: '#/components/schemas/CatalogCode' } }
          else:
            properties: { fraud_type: { type: 'null' } }
        - oneOf:
            - properties:
                evidence_source: { type: 'null' }
                evidence_reference: { type: 'null' }
                evidence_confidence: { type: 'null' }
            - properties:
                evidence_source: { $ref: '#/components/schemas/CatalogCode' }
                evidence_reference: { type: string, minLength: 1 }
                evidence_confidence: { type: number, minimum: 0, maximum: 1 }
        - if:
            properties: { truth_status: { enum: [LEGITIMATE, FRAUD_CONFIRMED] } }
          then:
            properties:
              evidence_source: { type: string }
              evidence_reference: { type: string }
              evidence_confidence: { type: number }
        - if:
            properties: { corrects_external_outcome_id: { type: 'null' } }
          then:
            properties: { reason_code: { const: INITIAL_OBSERVATION } }
          else:
            properties: { reason_code: { not: { const: INITIAL_OBSERVATION } } }
    CustomerActionReceiptV1:
      type: object
      additionalProperties: false
      required: [schema_version, receipt_id, assessment_id, business_event_id, received_at, retryable, customer_action_id, external_action_id, action, recommendation_alignment]
      properties:
        schema_version: { const: customer-action-receipt.v1 }
        receipt_id: { $ref: '#/components/schemas/UuidV7' }
        assessment_id: { $ref: '#/components/schemas/UuidV7' }
        business_event_id: { $ref: '#/components/schemas/UuidV7' }
        received_at: { $ref: '#/components/schemas/ServerInstant' }
        retryable: { const: false }
        customer_action_id: { $ref: '#/components/schemas/UuidV7' }
        external_action_id: { $ref: '#/components/schemas/ExternalId' }
        action: { enum: [ALLOWED, CHALLENGED, HELD, REVIEWED, BLOCKED, CANCELLED] }
        recommendation_alignment: { enum: [ALIGNED, NOT_ALIGNED, NOT_COMPARABLE] }
      allOf:
        - if:
            properties: { action: { const: CANCELLED } }
          then:
            properties: { recommendation_alignment: { const: NOT_COMPARABLE } }
          else:
            properties: { recommendation_alignment: { enum: [ALIGNED, NOT_ALIGNED] } }
    CustomerOutcomeReceiptV1:
      type: object
      additionalProperties: false
      required: [schema_version, receipt_id, assessment_id, business_event_id, received_at, retryable, outcome_id, external_outcome_id, corrects_external_outcome_id, truth_status, label_maturity]
      properties:
        schema_version: { const: customer-outcome-receipt.v1 }
        receipt_id: { $ref: '#/components/schemas/UuidV7' }
        assessment_id: { $ref: '#/components/schemas/UuidV7' }
        business_event_id: { $ref: '#/components/schemas/UuidV7' }
        received_at: { $ref: '#/components/schemas/ServerInstant' }
        retryable: { const: false }
        outcome_id: { $ref: '#/components/schemas/UuidV7' }
        external_outcome_id: { $ref: '#/components/schemas/ExternalId' }
        corrects_external_outcome_id:
          anyOf: [{ $ref: '#/components/schemas/ExternalId' }, { type: 'null' }]
        truth_status: { enum: [PENDING, INSUFFICIENT_EVIDENCE, LEGITIMATE, FRAUD_CONFIRMED] }
        label_maturity: { enum: [PROVISIONAL, MATURE] }
      x-ftid-cross-field: external_outcome_id must differ from non-null corrects_external_outcome_id
      allOf:
        - if:
            properties: { truth_status: { const: PENDING } }
          then:
            properties: { label_maturity: { const: PROVISIONAL } }
    AssessmentRecoveryResultV1:
      description: |
        The stored assessment exactly as the synchronous answer had it: event-assessment.v1
        for a v1 request, event-assessment.v1.1 for a v1.1 request. schema_version selects
        the form; each form is closed, so a payload never satisfies both.
      oneOf:
        - $ref: '../json-schema/event-assessment.v1.schema.json'
        - $ref: '../json-schema/event-assessment.v1.1.schema.json'
    FeedbackProblemV1:
      type: object
      additionalProperties: false
      required: [type, title, status, retryable, correlation_id]
      properties:
        type: { type: string, format: uri }
        title: { type: string }
        status: { type: integer }
        retryable: { type: boolean }
        correlation_id: { type: string, minLength: 1, maxLength: 128 }
      oneOf:
        - properties: { type: { const: 'https://fingertrackid.dev/problems/validation-failed' }, title: { const: 'Validation failed' }, status: { const: 400 }, retryable: { const: false } }
        - properties: { type: { const: 'https://fingertrackid.dev/problems/authentication-failed' }, title: { const: 'Autenticação falhou' }, status: { const: 401 }, retryable: { const: false } }
        - properties: { type: { const: 'https://fingertrackid.dev/problems/timestamp-out-of-window' }, title: { const: 'Timestamp outside window' }, status: { const: 401 }, retryable: { const: true } }
        - properties: { type: { const: 'https://fingertrackid.dev/problems/request-replayed' }, title: { const: 'Request replayed' }, status: { const: 401 }, retryable: { const: true } }
        - properties: { type: { const: 'https://fingertrackid.dev/problems/forbidden' }, title: { const: Forbidden }, status: { const: 403 }, retryable: { const: false } }
        - properties: { type: { const: 'https://fingertrackid.dev/problems/not-found' }, title: { const: 'Resource not found' }, status: { const: 404 }, retryable: { const: false } }
        - properties: { type: { const: 'https://fingertrackid.dev/problems/idempotency-conflict' }, title: { const: 'Idempotency conflict' }, status: { const: 409 }, retryable: { const: false } }
        - properties: { type: { const: 'https://fingertrackid.dev/problems/binding-conflict' }, title: { const: 'Binding conflict' }, status: { const: 409 }, retryable: { const: false } }
        - properties: { type: { const: 'https://fingertrackid.dev/problems/payload-too-large' }, title: { const: 'Payload too large' }, status: { const: 413 }, retryable: { const: false } }
        - properties: { type: { const: 'https://fingertrackid.dev/problems/unsupported-media-type' }, title: { const: 'Unsupported media type' }, status: { const: 415 }, retryable: { const: false } }
        - properties: { type: { const: 'https://fingertrackid.dev/problems/rate-limited' }, title: { const: 'Rate limited' }, status: { const: 429 }, retryable: { const: true } }
        - properties: { type: { const: 'https://fingertrackid.dev/problems/internal' }, title: { const: 'Internal error' }, status: { const: 500 }, retryable: { const: false } }
        - properties: { type: { const: 'https://fingertrackid.dev/problems/assessment-unavailable' }, title: { const: 'Assessment unavailable' }, status: { const: 503 }, retryable: { const: true } }
