A nomination lands at 09:12. The barge sails at 11:00. Somewhere in between, someone has to say the hull is clean, and a year from now someone else has to prove they checked.
That is the whole job. The integration that does it well is five decisions. Get them right and the code is about forty lines.
1. Where the answer lands
Decide the screen a person is looking at when they need the verdict. For a bunker supplier it is the order. For a charterer it is the nomination. For a broker it is the fixture recap.
Put the verdict there, with the date it was produced. A screening that runs in a nightly job and lands in a table nobody opens is a feed, and nobody says no to a barge because of a feed.
2. What you send
Two fields. The IMO number, and your own reference for the order.
curl https://arcnautical.com/api/v1/screenings \
-H "Authorization: Bearer $ARCNAUTICAL_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: screening:9274446:2026-09-15" \
-d '{"imo":"9274446","customer_reference":"order-48213"}'
The reference is echoed back on the record and is filterable later, which is how you find the screening behind one order when someone asks in 2031. Use the identifier your own system already has. Do not invent a new one.
include_vetting: false drops the A to E vetting grade and returns the sanctions verdict alone. It is faster, and it takes your calls out of the one queue that shares an upstream with everyone else. If your workflow only needs the sanctions answer, send it.
3. How long you wait
A screen reads several sanctions lists and, when you ask for vetting, port-state-control history too. The median is about nine seconds and the slow tail reaches eighteen. Set a 30 second timeout.
We say this because we have watched a real integration give up at eight seconds. On a normal day it was fine. Then a burst from another tenant pushed one request to 10.2 seconds and their system logged a timeout on a hull we had answered. The record existed, they had paid for it, and nobody on their side saw it.
A short timeout does not make the API faster. It makes your logs say "failed" on answers that arrived. If you must show something inside eight seconds, show "screening" and fill the verdict in when it lands.
4. What the call costs
A self-serve key carries 5,000 live screenings a month. What decides how fast you burn them is the Idempotency-Key header.
A replay under a key we have already answered returns the stored record and spends nothing, for 24 hours. So derive the key from the question, as in the example above: the hull and the date. A second order for the same hull on the same day replays free and instant. A fresh UUID per attempt makes every one of those a paid call.
const key = `screening:${imo}:${new Date().toISOString().slice(0, 10)}`;
On one integration we watch, 311 of 747 calls in a single day were same-day repeats of a hull already answered. Forty-two percent of the day's bill, for nothing new. The response would have told them: a repeat_screening notice names the earlier record and the key that would have replayed it.
Read notices[]. It is empty when there is nothing to say, so a non-empty array is always worth opening.
5. What you do with each verdict
sanctions.status has four values, and the fourth is the one most integrations mishandle.
| Verdict | Meaning | Your action |
|---|---|---|
| RED | A confirmed match on the vessel identifier, or a designated owner. | Stop. Show the match, its list and programme. Escalate. |
| AMBER | A name match without an identifier match, a watchlist listing, or an uncorroborated aggregated record. | Human review. Show what matched and why it is not RED. |
| GREEN | No match, and every mandatory list was read. | Proceed. Check coverage_gaps is empty first. |
| INCOMPLETE | A mandatory list or the vessel's identity was unavailable. No clearance can be defended. | Do not render as clean. Do not retry in a loop. It is not charged and it re-screens itself. |
INCOMPLETE deserves its own paragraph because it is where integrations quietly break. A hull we cannot name gets an INCOMPLETE, not a GREEN. The record carries a resolution block saying when we will re-screen it for you and where the answer will appear. You submit once. If you hold a confirmed name, send it as vessel_name and get an answer now.
One account we watch had 73 hulls standing INCOMPLETE at their last ask, and their worker never re-asked. A person found one of them by re-checking it by hand a day later. That is why the re-screen is ours to run now, not yours to remember.
Once it works, stop screening
Re-screening a book on a schedule is the most common way an integration spends 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?since= what moved. Or watch each hull:
curl https://arcnautical.com/api/v1/vessel-monitors \
-H "Authorization: Bearer $ARCNAUTICAL_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: monitor:9274446" \
-d '{"imo":"9274446","customer_reference":"supplier-book"}'
We re-screen every 24 hours, which is as often as the lists themselves publish, and fire vessel_risk.changed only when the sanctions status, ownership opacity or vetting grade moves. In both directions, so a hull you are waiting to clear tells you when it does. Fifty hulls on a self-serve key; each run costs one unit.
Webhook deliveries are signed with HMAC-SHA256 over the timestamp and body. On a test key a monitor is created but never scheduled, and POST /vessel-monitors/{id}/simulate fires a real event at your endpoint, which is how you prove the handler works before going live.
The errors you will see
Three matter in practice.
402 means the month's allowance is used up. Nothing ran and nothing was charged. The message states used and limit, the exact reset instant, and Retry-After is the seconds until then. Replays and reads keep working.
409 idempotency_conflict means you reused a key with a different body. Do not retry; that is the header doing its job. idempotency_in_progress means the same request is still running: wait Retry-After and send the same key again.
429 is a rate or concurrency limit. Honour Retry-After. Refused requests do not count against you.
The verdict is on the screen a person looks at, with its date. The request carries your order reference. The timeout is 30 seconds. The idempotency key is derived from hull and date. INCOMPLETE is never rendered as clean and never retried in a loop. A webhook handler has been proven with simulate on a test key. Someone reads notices[].
Everything above is on a self-serve key. Test and live run the same engine; test is a separate quota bucket, so development never draws on production allowance.
Start with the free call
curl https://arcnautical.com/api/v1/vessels/9274446/check needs no key. Then mint a key for the full record, or read the screening API page and the voyage playground if you also route cargo.