You have a column of IMO numbers. Maybe it is a week of bunker nominations, the hulls named in a charter party, or an export of port calls. Someone wants to know which of them are sanctioned before anything is signed.
The script below answers that. It needs Python 3.9 or later and requests, and nothing else: no account, no key.
One ship
The check is a public GET with the IMO number in the path:
import requests
r = requests.get("https://arcnautical.com/api/v1/vessels/9034690/check", timeout=30)
r.raise_for_status()
v = r.json()
print(v["vesselName"], v["sanctions"]["status"], "-", v["sanctions"]["detail"])
LUMA RED - 2 confirmed matches on vessel identifier.
That one call screened IMO 9034690 against OFAC SDN, the UK list, the EU Financial Sanctions Database, the UN Security Council list and OpenSanctions' maritime dataset. A hull someone checked in the last hour answers from cache at once. Three fresh screens we timed on 11 October took between 2 and 5 seconds, so the 30-second timeout is generous rather than necessary.
Read the answer the way an auditor will
sanctions.status has four values, not two, and the difference matters more than the code:
RED: a confirmed match on the vessel's identifier. Stop and get legal review.AMBER: a possible match a person needs to look at: a name shared with a listed ship, say, or a listing on a register that does not itself designate.INCOMPLETE: no match was found, but the screen cannot be called clear. A core list did not load, or no source could name the hull, or the number fails its check digit.detailsays which. Treat it as unanswered, never as clear.GREEN: no match on the lists that loaded. Ifsanctions.coverageCompleteisfalse, one supplementary list was unavailable, so re-screen before you rely on it.
So "clear" in code is status == "GREEN" and coverageComplete, and nothing else.
Catch typos before you send them
An IMO number carries its own checksum. Multiply the first six digits by 7, 6, 5, 4, 3 and 2, add them up, and the last digit of the sum must equal the seventh digit. For 9811000 (EVER GIVEN) that is 63 + 48 + 5 + 4 + 0 + 0 = 120, and the seventh digit is 0. A number that fails this belongs to no ship. It is almost always a typo, and screening it would tell you nothing, so the script checks first.
A whole list
Put the numbers in vessels.csv under a column called imo. "IMO 9811000" and "9811000" both work.
import csv
import time
import requests
API = "https://arcnautical.com/api/v1/vessels/{imo}/check"
def imo_is_valid(imo: str) -> bool:
"""The seventh digit of an IMO number is a check digit over the first six."""
if len(imo) != 7 or not imo.isdigit():
return False
total = sum(int(d) * w for d, w in zip(imo[:6], range(7, 1, -1)))
return total % 10 == int(imo[6])
def check(imo: str) -> dict:
for _ in range(5):
r = requests.get(API.format(imo=imo), timeout=30)
if r.status_code in (429, 503):
# Rate limited, or every screening slot busy: wait as told, then retry.
time.sleep(int(r.headers.get("Retry-After", "30")))
continue
r.raise_for_status()
return r.json()
raise RuntimeError(f"IMO {imo}: still refused after 5 attempts")
with open("vessels.csv", newline="") as f_in, open("screened.csv", "w", newline="") as f_out:
out = csv.writer(f_out)
out.writerow(["imo", "vessel_name", "sanctions", "clear", "detail"])
for row in csv.DictReader(f_in):
imo = row["imo"].strip().upper().removeprefix("IMO").strip()
if not imo_is_valid(imo):
out.writerow([imo, "", "INVALID_IMO", "no", "fails the IMO check digit; fix it before screening"])
continue
v = check(imo)
s = v["sanctions"]
clear = s["status"] == "GREEN" and s["coverageComplete"]
out.writerow([imo, v.get("vesselName") or "", s["status"], "yes" if clear else "no", s["detail"]])
time.sleep(1)
Four rows in, four rows out (the GREEN detail is shortened here):
imo,vessel_name,sanctions,clear,detail
9034690,LUMA,RED,no,2 confirmed matches on vessel identifier.
9811000,EVER GIVEN,GREEN,yes,"No matches across OFAC SDN, OpenSanctions, EU FSD, UN Consolidated, UK OFSI ..."
9703318,MSC ZOE,GREEN,yes,"No matches across OFAC SDN, OpenSanctions, EU FSD, UN Consolidated, UK OFSI ..."
9295345,,INVALID_IMO,no,fails the IMO check digit; fix it before screening
Two things in check() are worth keeping if you rewrite it. First, it honours Retry-After. The free check allows 100 requests an hour per IP address, and the X-RateLimit-Remaining header on every response tells you how many you have left. Second, a 503 means every shared screening slot was busy. It does not count against your allowance, and waiting the number of seconds it gives is all it asks.
What the free check leaves out
The keyless answer is a verdict, not evidence. It does not say which list matched, under which programme, or with what confidence. It is also a sample for evaluation and occasional lookups, not a feed: past 100 hulls an hour, or for a product that runs on it, you want a key. A key is free and needs no card. The same question on a key returns the full record:
import datetime
import os
import requests
imo = "9034690"
r = requests.post(
"https://arcnautical.com/api/v1/screenings",
headers={
"Authorization": f"Bearer {os.environ['ARCNAUTICAL_API_KEY']}",
# The same hull asked again today replays the stored record instead of spending a unit.
"Idempotency-Key": f"screening:{imo}:{datetime.date.today()}",
},
json={"imo": imo},
timeout=30,
)
r.raise_for_status()
record = r.json()
That record lists every match with its list, programme and confidence class, reports each source separately, and is kept so you can cite it later. The same key screens batches of 50 and can watch hulls for you, so the script above does not have to run again next week. The API docs cover all of it.
What it is not
- It screens vessels, not companies or people.
- It matches the IMO number, the current name and the other names our sources hold for the hull. A former name none of our sources holds is not screened, so a ship renamed after a designation under a name we never saw can still read
GREEN. - It is not legal advice. It is a screening result with its sources named, which is what your counsel will ask for.
Run it now
Paste curl https://arcnautical.com/api/v1/vessels/9034690/check into a terminal. The response fields are documented in the API docs, and the vessel-check-api README has the same examples in JavaScript and Google Sheets.