---
title: "Screen a hull by IMO (no key)"
method: GET
path: "/api/v1/vessels/{imo}/check"
tags: ["Public vessel check"]
---

# Screen a hull by IMO (no key)

`GET /api/v1/vessels/{imo}/check`

## Request

- Base URL: `https://arcnautical.com`
- URL: `https://arcnautical.com/api/v1/vessels/{imo}/check`
- Auth: none required

Screens one hull against sanctions, ownership opacity, and vetting. Requires NO API key and no account. Rate-limited to 100 requests/hour per IP and cached for one hour. The response reports X-RateLimit-Limit, -Remaining and -Reset so a client can pace itself without being refused first. Results are not retained against a customer and carry no quota. This screens a VESSEL, not a voyage — voyage scoring is POST /api/v1/voyage-assessments and needs a key.

## Path parameters

- `imo` string, required

## Response `200`

Screening verdict.

- PublicVesselCheck: Screening verdict for one hull. Returned by the keyless GET /api/v1/vessels/{imo}/check.
  - `imo` string, required: The seven-digit IMO that was screened.
  - `vesselName` string, nullable: The name our sources hold for this hull, so you can confirm WHICH vessel was screened. Null when no source names it.
  - `alsoRecordedAs` object[]: Other names our sources hold for this hull, each with the source that saw it. Every one was screened on this call.
    - `name` string
    - `source` string
  - `identityBasis` 'sources' | 'user' | 'none': sources = our records name the hull; none = nothing named it (the identifiers were screened, a name was not).
  - `checkDigitValid` boolean: False when the IMO fails its check digit — almost always a typo. The hull is still screened; treat a clear result with suspicion.
  - `sanctions` PublicSanctionsVerdict, required: 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.
    - `status` 'RED' | 'AMBER' | 'INCOMPLETE' | 'GREEN', required: 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` string, required: Human-readable explanation of the verdict.
    - `coverageComplete` boolean, required: 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` string[]: Display names of sources that did NOT load. One of: OFAC SDN, OpenSanctions, EU FSD, UN Consolidated, UK OFSI.
    - `detailKey` string: 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` object: Parameters for `detailKey`, such as the match count.
    - `identityScope` 'current-name-and-identifiers': 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.
  - `ownership` PublicOwnership, required: 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.
    - `opacity` 'LOW' | 'MEDIUM' | 'HIGH' | 'null', nullable
    - `score` integer, nullable
  - `ownershipFactors` object[], nullable: 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.
    - `label` string, required
    - `points` number, required: Contribution to ownership.score. Negative for a relief.
  - `vetting` PublicVetting, required: 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.
    - `grade` 'A' | 'B' | 'C' | 'D' | 'E' | 'null', nullable
    - `score` integer, nullable
    - `status` 'acceptable' | 'marginal' | 'substandard' | 'unacceptable' | 'not_assessed'
  - `assessed` boolean, required: 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` PublicCurrentContext: Freshness of INCIDENT CONTEXT for this hull. Distinct from sanctions.coverageComplete, which is about sanctions-source coverage.
    - `coverage` 'fresh' | 'refreshing' | 'stale' | 'unavailable': How recently incident context was refreshed for this hull.
    - `checkedAt` string, date-time, nullable
    - `lookbackStart` string
    - `incidents` object[]
  - `checkedAt` string, date-time, required
  - `provider` string
  - `fullReport` string: Human-readable report URL for this hull.
  - `docs` string: Documentation URL for this endpoint.
  - `tier` 'keyless': 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` string: 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` string: Where the authenticated screening record is documented.

## Other responses

- `400`: `invalid_imo`: the path segment does not contain exactly one 7-digit number. The message says what is wrong with the value sent (no digits, too few, or which valid IMO an 8-digit value contains). Not counted against the hourly allowance, and it carries the X-RateLimit headers. Before 2026-09-30 a value containing a letter was answered 404 `not_found` by the edge instead.
- `429`: Rate limit exceeded: the per-IP hourly allowance, or a burst refused before it. Also code `concurrency_limit`: more than 2 checks from this address waiting for a screening slot at once; send the next when one returns. All carry Retry-After and the {code, message, request_id} envelope.
- `500`: Screening failed.
- `503`: `keyless_busy`: at most 4 hulls that are not already cached are screened at once across every keyless caller, and this check could not be seated within 10s. It is not counted against the hourly allowance; retry after Retry-After. A hull anyone checked in the last hour answers from cache and never waits. `keyless_paused`: keyless checks are temporarily paused under unusual load; Retry-After says when to try again. Neither applies to the keyed lane (POST /api/v1/screenings), which runs separately.

---

[API](https://skmtc.dev/arcnautical/apis/arcnautical-api.md) · [All operations](https://skmtc.dev/arcnautical/apis/arcnautical-api/llms.txt) · [OpenAPI document](https://skmtc.dev/arcnautical/apis/arcnautical-api/revisions/40cd2659cdd7?raw)
