{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/arcnautical-com/json-schema/arcnautical-com-public-vessel-check-schema.json",
  "title": "PublicVesselCheck",
  "description": "Screening verdict for one hull. Returned by the keyless GET /api/v1/vessels/{imo}/check.",
  "x-generated": "2026-11-02",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/arcnautical-com-openapi.json#/components/schemas/PublicVesselCheck",
  "type": "object",
  "required": [
    "imo",
    "sanctions",
    "ownership",
    "vetting",
    "assessed",
    "checkedAt"
  ],
  "properties": {
    "imo": {
      "type": "string",
      "description": "The seven-digit IMO that was screened."
    },
    "vesselName": {
      "type": [
        "string",
        "null"
      ],
      "description": "The name our sources hold for this hull, so you can confirm WHICH vessel was screened. Null when no source names it."
    },
    "alsoRecordedAs": {
      "type": "array",
      "description": "Other names our sources hold for this hull, each with the source that saw it. Every one was screened on this call.",
      "items": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "source": {
            "type": "string"
          }
        }
      }
    },
    "identityBasis": {
      "type": "string",
      "enum": [
        "sources",
        "user",
        "none"
      ],
      "description": "sources = our records name the hull; none = nothing named it (the identifiers were screened, a name was not)."
    },
    "checkDigitValid": {
      "type": "boolean",
      "description": "False when the IMO fails its check digit — almost always a typo. The hull is still screened; treat a clear result with suspicion."
    },
    "sanctions": {
      "$ref": "#/$defs/PublicSanctionsVerdict"
    },
    "ownership": {
      "$ref": "#/$defs/PublicOwnership"
    },
    "ownershipFactors": {
      "type": [
        "array",
        "null"
      ],
      "description": "What ownership.score is made of: the lines it is the sum of (clamped to 0-100), each with its points, so \"no LEI record on an open register\" can be told from a hidden owner. Null when the hull was not assessed, and null when the lines would not add up to the score shown (we never send a breakdown that disagrees with its total). A record cached before 2026-10-02 omits it. Labels are sentences for a person to read, not codes: display them, do not parse them.",
      "items": {
        "type": "object",
        "required": [
          "label",
          "points"
        ],
        "properties": {
          "label": {
            "type": "string"
          },
          "points": {
            "type": "number",
            "description": "Contribution to ownership.score. Negative for a relief."
          }
        }
      }
    },
    "vetting": {
      "$ref": "#/$defs/PublicVetting"
    },
    "assessed": {
      "type": "boolean",
      "description": "False means the hull was not assessed: ownership opacity/score and the vetting grade/score are then NULL and vetting.status is \"not_assessed\" — there is no stand-in value. Render \"not assessed\", never a grade. The sanctions verdict is unaffected."
    },
    "currentContext": {
      "$ref": "#/$defs/PublicCurrentContext"
    },
    "checkedAt": {
      "type": "string",
      "format": "date-time"
    },
    "provider": {
      "type": "string"
    },
    "fullReport": {
      "type": "string",
      "description": "Human-readable report URL for this hull."
    },
    "docs": {
      "type": "string",
      "description": "Documentation URL for this endpoint."
    },
    "tier": {
      "type": "string",
      "enum": [
        "keyless"
      ],
      "description": "Always \"keyless\" here. Says which tier produced the body, so a client that also holds a key can tell the two record shapes apart."
    },
    "tierDetail": {
      "type": "string",
      "description": "What this tier does NOT contain, that a self-serve key is free, what it grants, and where to mint one. Rides on every response because this endpoint is the one channel a keyless integration is guaranteed to read."
    },
    "fullRecord": {
      "type": "string",
      "description": "Where the authenticated screening record is documented."
    }
  },
  "$defs": {
    "PublicCurrentContext": {
      "type": "object",
      "description": "Freshness of INCIDENT CONTEXT for this hull. Distinct from sanctions.coverageComplete, which is about sanctions-source coverage.",
      "properties": {
        "coverage": {
          "type": "string",
          "enum": [
            "fresh",
            "refreshing",
            "stale",
            "unavailable"
          ],
          "description": "How recently incident context was refreshed for this hull."
        },
        "checkedAt": {
          "type": [
            "string",
            "null"
          ],
          "format": "date-time"
        },
        "lookbackStart": {
          "type": "string"
        },
        "incidents": {
          "type": "array",
          "items": {
            "type": "object"
          }
        }
      }
    },
    "PublicOwnership": {
      "type": "object",
      "description": "Ownership opacity. HIGH is the dark-fleet signature (no LEI plus flag of convenience). Both fields are NULL when the top-level `assessed` is false.",
      "properties": {
        "opacity": {
          "type": [
            "string",
            "null"
          ],
          "enum": [
            "LOW",
            "MEDIUM",
            "HIGH",
            null
          ]
        },
        "score": {
          "type": [
            "integer",
            "null"
          ],
          "minimum": 0,
          "maximum": 100
        }
      }
    },
    "PublicSanctionsVerdict": {
      "type": "object",
      "description": "Sanctions outcome plus how complete the SOURCE COVERAGE was. Note: coverageComplete/coverageGaps describe which sanctions lists were reachable. That is a different axis from currentContext.coverage, which describes incident-context freshness. Same word, different meaning.",
      "required": [
        "status",
        "detail",
        "coverageComplete"
      ],
      "properties": {
        "status": {
          "type": "string",
          "enum": [
            "RED",
            "AMBER",
            "INCOMPLETE",
            "GREEN"
          ],
          "description": "RED = confirmed match. AMBER = possible match needing review. GREEN = no match against the sources reached. INCOMPLETE = no match, but the screen cannot be attested as clear: a CORE source (OFAC SDN, OpenSanctions, EU FSD) could not be loaded, OR no source could name the hull (the identifiers were screened, a name was not), OR the IMO fails its check digit and belongs to no vessel. `detail` says which. INCOMPLETE FAILS CLOSED — it must never be read or rendered as clear, and it is deliberately never cached."
        },
        "detail": {
          "type": "string",
          "description": "Human-readable explanation of the verdict."
        },
        "coverageComplete": {
          "type": "boolean",
          "description": "True when every sanctions source loaded. A GREEN with coverageComplete=false means \"clear against the sources we reached, one supplementary source was unavailable\" — treat it as a distinct state and re-screen, not as a clean pass."
        },
        "coverageGaps": {
          "type": "array",
          "description": "Display names of sources that did NOT load. One of: OFAC SDN, OpenSanctions, EU FSD, UN Consolidated, UK OFSI.",
          "items": {
            "type": "string"
          }
        },
        "detailKey": {
          "type": "string",
          "description": "Stable message key behind `detail` (e.g. verdict.sanctions.confirmed_match_other), for a client that renders its own wording or another language. `detail` is the English rendering of this key with `detailParams`."
        },
        "detailParams": {
          "type": "object",
          "description": "Parameters for `detailKey`, such as the match count.",
          "additionalProperties": true
        },
        "identityScope": {
          "type": "string",
          "enum": [
            "current-name-and-identifiers"
          ],
          "description": "What the lists were matched against: the identifiers and current name of the hull, plus the other names our identity sources hold for it (named in `detail`). A former name none of our sources holds is not screened, so a hull renamed after a designation under a name we never saw can still read GREEN. The value keeps its original spelling for compatibility."
        }
      }
    },
    "PublicVetting": {
      "type": "object",
      "description": "Vetting grade. Both `grade` and `score` are NULL when the top-level `assessed` is false — there is no stand-in grade, and a letter beside `assessed: false` gets read as a letter. They were declared non-nullable here until 2026-09-06 while the endpoint already returned null, so a client generated from this document could not deserialise the response.",
      "properties": {
        "grade": {
          "type": [
            "string",
            "null"
          ],
          "enum": [
            "A",
            "B",
            "C",
            "D",
            "E",
            null
          ]
        },
        "score": {
          "type": [
            "integer",
            "null"
          ],
          "minimum": 0,
          "maximum": 100
        },
        "status": {
          "type": "string",
          "enum": [
            "acceptable",
            "marginal",
            "substandard",
            "unacceptable",
            "not_assessed"
          ]
        }
      }
    }
  }
}