On 12 September we ran the searches a compliance lead would run: API to check if a vessel is sanctioned by IMO number, OFAC vessel screening API, sanctions screening API for ships. Six vendors came back on the first page. Each one puts a key, a quote or a demo booking in front of the first request. The ones that publish a price start at $60 a month for the entry tier.

We put the first request in front of the key instead.

The call

curl https://arcnautical.com/api/v1/vessels/9274446/check

That is the whole thing. No key and no account. Here is the response, trimmed, as it came back this morning:

{
  "imo": "9274446",
  "sanctions": {
    "status": "RED",
    "detail": "4 confirmed matches on vessel identifier.",
    "coverageComplete": true,
    "coverageGaps": []
  },
  "ownership": { "opacity": "MEDIUM", "score": 40 },
  "vetting": { "grade": "E", "score": 90, "status": "unacceptable" },
  "assessed": true,
  "checkedAt": "2026-09-15T03:11:41.221Z"
}

The hull is screened against OFAC SDN, OpenSanctions, the EU financial sanctions database, the UN consolidated list and UK OFSI. The ownership line is our opacity score for the corporate chain behind the vessel. The vetting line is an A to E grade built from port-state-control history and the sanctions result. Both of those are computed by us from the chain and the history. They are not copied off a list.

The ceiling is 100 requests an hour per IP, and it is in the response headers: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset. The 100 in that header is read from the same table that refuses the 101st request. We do it that way because we got it wrong. For a while our OpenAPI description said 200 an hour, the server enforced 100, and nobody noticed until a third-party docs renderer put the two next to each other. A number a customer plans against now comes from the enforcing file or it does not get typed at all.

A refused request does not count against you. That one we also learned the hard way: a caller working through their book at about 110 an hour went from 60% refused at six in the morning to 100% refused by eight, and stayed there, because every refusal was landing in the sliding window and pushing it forward. The ceiling was doing its job and the accounting made it permanent. Refusals are free now, and the 429 body says that a key exists and lifts the limit, which is the one fact the person hitting it needs.

CORS is open on this endpoint, so it also works from a page in a browser.

What the key adds

The keyless check returns the verdict and throws the working away. POST /api/v1/screenings runs the same engine on your key and keeps all of it: every sanctions match with its source, programme and confidence class, the five weighted vetting factors, and a sources[] array that says which lists were read and when. Same hull, on a key:

{
  "id": "dfa1bde9-...",
  "imo": "9274446",
  "vessel_name": "HS STAR",
  "sanctions": {
    "status": "RED",
    "matches": [
      { "name": "HS STAR", "source": "ofac_sdn",
        "programs": ["RUSSIA-EO14024"],
        "match_type": "exact_imo", "confidence_class": "confirmed" }
    ],
    "coverage_complete": true
  },
  "ownership": { "opacity": "MEDIUM", "score": 40 },
  "vetting": { "grade": "E", "status": "unacceptable", "detention_count": 1, "factors": [ ... ] },
  "sources": [ { "source": "ofac_sdn", "status": "ok", "checked_at": "..." }, ... ],
  "customer_reference": "order-48213"
}

Read sources[] before you act on a GREEN. status: "error" on a row means that list did not load, and a clean result built on a list we could not read is not a clean result. The response says so in coverage_gaps instead of going quiet.

The fourth verdict

sanctions.status has four values. RED, AMBER and GREEN are the three you expect. The fourth is INCOMPLETE, and it took longer to get right than the other three put together.

Name matching is half of sanctions screening. We resolve a hull's name from five stores we hold (our own register, an AIS-derived registry, and three port-state-control stores from the Paris and Tokyo MoUs), and when all five are silent we read four public registers live, inside the request: DNV's vessel register, the ITF ship lookup, the IACS vessels-in-class file and USCG PSIX. New-builds and recently renamed hulls are the ones that fall through. When none of that produces a name, the name half cannot run, and we return INCOMPLETE. A clearance we cannot defend is worse than no answer.

An identity INCOMPLETE is not charged. If you can confirm a name, send vessel_name with the request and the answer comes back at once, recorded in the audit trail as caller-supplied. Or do nothing. The hull goes to the head of our next scheduled port-state fetch, we re-screen the same request on a bounded schedule, and the verdict arrives as a new record linked to the first one. You submit once.

Two checks run on the number itself before any list is read. The seventh digit of an IMO number is a checksum over the first six, so a number that fails it belongs to no vessel, and the usual cause is two digits transposed off a nomination. And a number claimed by many unrelated hulls in AIS is a number our data no longer identifies. 1234567 is claimed by fifteen different vessels in our registry, among them a website and a piece of test equipment. A screening API that hands you a confident name and a clean verdict for 1234567 is showing you what it would do with a real hull, and you should believe it.

What it costs, and how to pay less

A self-serve key carries 5,000 live screenings a month and 500 test. There is no card to enter and no clock running on it. Neither environment expires, and both run the same engine; test is a separate quota bucket, idempotency namespace and webhook namespace, and that is the only difference.

The thing that decides your bill is the Idempotency-Key header, which every POST requires. A replay under a key we have already answered returns the stored response, spends no unit, and is kept for 24 hours. So the key is your deduplication lever. Generate a fresh UUID per attempt and two questions about the same hull on the same day are two questions. Derive it from the question instead, screening:9274446:2026-09-15, and every later ask that day is free and instant.

On one integration we measured, 42% of a month's screenings were same-day repeats of a hull already answered. Every one of them was billable only because the client generated a random key.

Stop re-screening the book

Re-asking the same IMO on a schedule is the most common way an integration burns its allowance on answers it already holds. Two endpoints exist so you do not have to.

Put the hulls in a fleet and ask GET /api/v1/fleets/{id}/changes what moved since a cursor. Or POST /api/v1/vessel-monitors for a standing subscription per hull: we re-screen every 24 hours and fire vessel_risk.changed only when the sanctions status, ownership opacity or vetting grade moves. It fires in both directions, so a hull you are waiting to clear tells you when it does. Fifty hulls on a self-serve key, and each run costs one unit.

The 24 hours is set by the data. OFAC publishes daily at best, so a six-hour cadence would spend four times the quota re-reading the same file.

The record you can hand an auditor

A screening is not a cached computation. It is the record of a compliance decision, and OFAC extended its recordkeeping requirement from five years to ten in March 2025, with sanctions screening records named explicitly. We keep every screening for ten years.

When someone asks for everything we screened in Q3, POST /api/v1/exports builds one file, CSV or JSONL, usually inside a minute. The SHA-256 is published on the resource and in a response header, so years later you can show that the copy in your evidence folder is the copy we generated. A range over 500,000 rows is refused with the real count in the error. We will not hand you a file that is quietly short, because everyone downstream of whoever reads the flag would treat it as complete.

Voyage risk on the same key

If you route cargo as well as vet hulls, the same key scores voyages. POST /api/v1/voyage-assessments takes two UN/LOCODEs and returns a route-level score, ranked drivers, confidence and missing sources. A live key carries 10,000 assessments a month and 10 corridor monitors recomputed every six hours. There is a browser playground that runs the production engine with no account, and in July we wrote up how to add it to a supply-chain platform and how to read the response.

Where you can call it from

The OpenAPI 3.1 document is at /developers/reference/, and there is a public Postman workspace linked from the docs. The @arcnautical/mcp package on npm wraps the API as tools for Claude, ChatGPT, Cursor or any MCP client. Two of those tools, check_vessel and find_port, need no key, so an assistant with the server installed can answer is IMO 9274446 sanctioned? with nothing configured. If you use a coding agent, /developers/prompt.md is a one-line onboarding prompt that wires the integration up itself.

What it is not

It is not a live AIS tracker. We hold an AIS-derived registry to name hulls; we do not stream positions to you.

It is not fast the way a cache is fast. A screen reads several upstream lists and typically takes 5 to 15 seconds. Give it a 30-second timeout. A client that gives up at 8 seconds will log timeouts on hulls we answered.

It is not legal advice. It is a screening record with every source named and dated, which is what your counsel will ask you for.

And it will not call an unread list a clean one. See sources[].

Get a key

Create an account, open Developer API, and mint a key with environment test or live. It is shown once, so put it in a secret manager straight away. If you would rather read first, the vessel sanctions screening API page walks through the keyless check, the screening record, fleets and monitors in the order a buyer meets them, and the developer docs carry the full contract in cURL, JavaScript, Python, Java and C#.

If you need more than 5,000 screens a month, tell us what you are building and we will provision it.

Run the first call now

Paste curl https://arcnautical.com/api/v1/vessels/9274446/check into a terminal. Then mint a key for the full record, or read the screening API page first.