---
title: "Screen a cargo"
method: POST
path: "/api/v1/cargo-screenings"
tags: ["Cargo screening"]
---

# Screen a cargo

`POST /api/v1/cargo-screenings`

## Request

- Base URL: `https://arcnautical.com`
- URL: `https://arcnautical.com/api/v1/cargo-screenings`
- Auth: one of:
  - HTTP bearer
  - API key in header `X-ArcNautical-Key`

Screens a goods description, and an HS code when you have one, against the Common High Priority Items List (the 50 six-digit HS headings the US, EU, UK and Japan jointly identify as items Russia seeks for its weapons programmes), against the discharge jurisdiction, and — for medicine and medical devices — against the published humanitarian general-licence context. Returns OK, WARNING or CRITICAL.

Every response names the lists it read in lists_screened AND the lists it did NOT read in lists_not_screened. This is NOT an export-control classification: it does not assign an ECCN or an EU dual-use category and it does not assess end use or end user. Read lists_not_screened before relying on an OK.

Supplying imo carries that hull last known sanctions verdict onto the response so cargo risk and vessel risk are read together. It never screens the vessel for you: an unscreened hull is reported as unscreened, never as clear.

Required scope: cargo:screen.

## Headers

- `Idempotency-Key` string, required

## Request body

- CreateCargoScreeningRequest
  - `goods_description` string, required: What the cargo is, e.g. "ball bearings for main engine".
  - `hs_code` string: Six-digit HS heading or longer national code. When supplied it is authoritative and the description is not keyword-matched.
  - `discharge_country` string: ISO 3166-1 alpha-2 discharge country. Moves the verdict, so membership is enforced, not just shape: XX, SU, YU, AN and UK all return 422 with the replacement named (UK is not a code — the United Kingdom is GB). Omit the field entirely if the destination is genuinely unknown; the response then says no jurisdiction assessment was made, rather than reporting the destination as clean.
  - `imo` string: Optional hull, to carry its last known sanctions verdict onto the response.
  - `customer_reference` string: Your own identifier, e.g. the purchase order.

## Response `201`

Cargo screened.

- CargoScreening
  - `id` string, uuid, required
  - `status` 'OK' | 'WARNING' | 'CRITICAL', required: The SHIPMENT decision — the worse of the cargo verdict and the named hull last known sanctions verdict. Clean cargo onto a sanctioned hull is not an OK. Act on this field.
  - `status_basis` 'cargo' | 'vessel': Which side drove the top-line status.
  - `cargo_status` 'OK' | 'WARNING' | 'CRITICAL': The goods verdict alone, ignoring the vessel.
  - `goods_description` string, required
  - `hs_code` string, nullable
  - `customer_reference` string, nullable
  - `match_basis` 'hs_code' | 'description' | 'null', nullable
  - `matches` CargoMatch[]
    - `list` 'CHPL'
    - `hs6` string
    - `tier` '1' | '2' | '3.A' | '3.B' | '4.A' | '4.B': CHPL tier. 1 is the highest concern.
    - `description` string
    - `basis` 'hs_code' | 'description': hs_code is an exact heading match. description is a KEYWORD HEURISTIC and is not a classification.
  - `destination` CargoDestination
    - `class` 'comprehensive' | 'high_risk' | 'diversion_risk' | 'standard'
    - `label` string
    - `basis` string: Why this classification, naming the programme or guidance.
  - `humanitarian` CargoHumanitarian: Medicine and medical-device carve-outs, screened against OFAC's published lists per programme. The three comprehensive programmes do not work the same way: Iran (31 CFR 560.530(a)(3)(i)) is a self-executing general licence carving out a device list AND seven medicine classes; North Korea (31 CFR 510.521) carves out a DIFFERENT device list, published 11 June 2026, and has no medicine-class exclusion at all; Cuba has no self-executing general licence — the export needs a Department of Commerce licence first.
    - `medical_goods` boolean
    - `medical_goods_basis` 'inn_drug_name' | 'medical_term' | 'inn_stem' | 'ofac_device_list' | 'null', nullable: What made it read as medical. A keyword heuristic over free text, never a classification.
    - `general_licence_status` 'covered' | 'excluded' | 'review_required' | 'no_general_licence' | 'not_applicable': covered = medical goods, a general licence exists, nothing excluded it. excluded = a published exclusion names it. review_required = in scope of an exclusion this screen cannot settle. no_general_licence = medical goods into a programme with no self-executing general licence. not_applicable = not medical goods, or no comprehensive programme.
    - `general_licence` string, nullable: The general licence this item was ASSESSED AGAINST. It stays populated when general_licence_status is `excluded`, because naming which licence fails to reach the item is the useful half of that answer — it is not an authorisation. Null means the programme has no self-executing general licence at all (Cuba), not that one is unknown.
    - `exclusions` CargoExclusion[]
      - `kind` 'medical_device' | 'medicine_class', required
      - `authority` string, required: The clause that produced this, e.g. 31 CFR 560.530(a)(3)(ii) for the device list or (a)(3)(iii) for the medicine classes.
      - `published_text` string, required: The published list line or class term, verbatim, so the finding can be checked against the source.
      - `matched_text` string, required: The phrase in your own goods_description that produced this finding.
      - `certainty` 'named' | 'probable', required: named = the description says what the list says. probable = in scope, but a published qualifier decides it and you did not state it.
      - `effect` 'excluded' | 'review', required
      - `threshold` object, nullable: Present where the published line carries a numeric qualifier, e.g. pumps over 1 litre/minute, autoclaves over 20 litres.
        - `quantity` 'flow_rate' | 'volume' | 'area' | 'temperature' | 'purity'
        - `operator` string
        - `value` number
        - `unit` string
        - `stated_value` number, nullable: The value read out of your description, in the list line unit. Null means you did not state one.
        - `resolves_with` string: What to supply to turn this from review into an answer.
      - `note` string, nullable
    - `conditions` string[]: The conditions on the licence. A general licence for medicine does NOT cover every medicine, nor any end user.
    - `list_edition` CargoListEdition: Which edition of the OFAC list answered. A compliance control whose data version is invisible cannot be audited, and a stale list is otherwise indistinguishable from a current one.
      - `name` string
      - `authority` string
      - `published` string: The edition as OFAC labels it, e.g. "updated February 2, 2017".
      - `source_url` string, uri
      - `source_sha256` string: sha256 of the published artefact this snapshot was taken from.
      - `retrieved_at` string, date-time
      - `item_count` integer
      - `lines_without_matcher` integer: Published lines this screen has no automated matcher for and did NOT test your description against. Read the source list for these.
  - `vessel` object, nullable: Present when imo was supplied. A null sanctions_status with a note means the hull is UNSCREENED, which is not the same as clear.
    - `imo` string
    - `sanctions_status` string, nullable
    - `screening_id` string, uuid, nullable
    - `screened_at` string, date-time, nullable
    - `note` string
  - `lists_screened` string[], required: Exactly what this screen read.
  - `lists_not_screened` string[], required: Exactly what it did NOT read. Read this before relying on an OK.
  - `methodology_version` string
  - `screened_at` string, date-time
  - `expires_at` string, date-time
  - `disclaimer` string

## Other responses

- `400`: Validation error. `code` is one of: - `missing_idempotency_key` — A POST arrived with no Idempotency-Key header. - `invalid_idempotency_key` — The Idempotency-Key was present but unusable (empty or over-long). - `invalid_json` — The body did not parse as JSON.
- `401`: Missing or invalid API key. `code` is one of: - `unauthorized` — No credential was supplied. - `invalid_api_key` — The key is unknown, revoked, expired, or its prefix does not match the environment.
- `402`: The allowance for this resource is used up: the month's, or a one-time total (live screenings on an account opened from 2026-10-01, or a pilot), which does not reset (then there is no reset instant and no Retry-After). The refused request is NOT metered. message states used/limit and the reset instant when there is one; a replay under an Idempotency-Key already answered is still free and is not metered, reads are never metered, and a test-environment key has its own allowance. `code` is one of: - `quota_exhausted` — The allowance for this resource is spent: the month's (GET /api/v1/usage carries the reset date), or a one-time total (live screenings on an account opened from 2026-10-01, or a pilot), which does not reset.
- `403`: The key is not entitled to this environment or scope. `code` is one of: - `insufficient_scope` — The key does not hold the scope this endpoint requires. - `entitlement_inactive` — The account has no active entitlement. - `ip_not_allowed` — The key is IP-restricted and this source address is not on its list. - `monitoring_unavailable_in_test` — Vessel and corridor monitors are not available on a test key. - `simulation_unavailable_in_live` — Simulated monitor events are available only on a test key. A synthetic risk change on a live monitor would enter your compliance record as a real finding about a real vessel.
- `404`: Resource not found. `code` is one of: - `not_found` — No such resource, or it is not yours. A malformed id answers this way too, deliberately. - `unsupported_api_version` — The path names an /api/vN this service does not publish. Answered by the edge, so it is the one code that carries no rate-limit headers. NEVER retry: our own guidance says an HTML body means a network layer answered and should be retried, and this exists so that advice is not applied to a permanent 404.
- `405`: The path exists but not with this method. The Allow header names the methods that do. `code` is one of: - `method_not_allowed` — The path exists but not with this verb. The Allow header names the verbs that do work.
- `409`: Idempotency conflict. `code` is one of: - `idempotency_conflict` — This Idempotency-Key was already used with a different body. Never resolves — use a new key. - `idempotency_in_progress` (retryable) — The original request with this key is still running, and this retry waited for it (up to 30s) without it finishing. Retry with the same key: the next retry waits again and returns the original result the moment it lands, flagged X-ArcNautical-Idempotent-Joined: true. Carries Retry-After. - `export_not_ready` (retryable) — The export job has not finished. Poll it; carries Retry-After. - `delivery_in_flight` (retryable) — That webhook delivery is being attempted right now. - `endpoint_disabled` — The webhook endpoint is disabled. Re-enable it first. - `endpoint_deleted` — The webhook endpoint no longer exists. - `monitor_limit_exceeded` — Corridor monitor ceiling reached. Delete one first. - `vessel_monitor_limit_exceeded` — Vessel monitor ceiling reached. Delete one first. - `vessel_monitor_exists` — A monitor already watches this hull.
- `415`: Content-Type describes a body that is not JSON (form-encoded, multipart, XML). application/json, text/plain and an absent header are all accepted. `code` is one of: - `unsupported_media_type` — Content-Type declared something other than application/json. Omitting the header is accepted; declaring the wrong one is not.
- `422`: Request or route validation failed. Also returned for a body field this endpoint does not read — field_violations names it and lists the accepted keys. `code` is one of: - `validation_failed` — One or more fields are invalid; field_violations names each one. - `route_not_found` — Both ports are known but no sea route connects them. - `export_too_large` — The requested range exceeds the per-export row cap. Narrow the filters and export in parts. - `webhook_url_blocked` — The URL resolves to a private, loopback or link-local address. - `webhook_url_unresolvable` — The URL host does not resolve.
- `429`: Rate limit exceeded. `code` is one of: - `rate_limit_exceeded` (retryable) — Over the per-key request rate. Shed RATE; back off to X-RateLimit-Reset. - `concurrency_limit` (retryable) — Over your own in-flight ceiling. Shed CONCURRENCY, or use the batch endpoint. Retry-After carries a live estimate, not a fixed number. - `service_concurrency_limit` (retryable) — The shared pool is full and your account is within its allowance. A graded screen QUEUES for that pool first and is served round-robin between accounts, so this means the queue itself ran out of time — you were not stuck behind one customer's batch. Reducing your fan-out will NOT help; retry unchanged after Retry-After. include_vetting: false skips this queue entirely, and /api/v1/screening-batches is not subject to it at all. - `export_concurrency_limit` (retryable) — Too many exports building at once. Wait for one to finish.
- `500`: Internal or upstream error. `code` is one of: - `internal_error` (retryable) — An unhandled failure. Retry with backoff; quote request_id if it persists. - `assessment_failed` (retryable) — Voyage scoring failed to produce a result. - `screening_failed` (retryable) — The screening engine failed to produce a verdict. No unit is charged. - `export_failed` (retryable) — The export job failed while building. - `export_row_count_grew` — The matching rows grew past the cap while the file was being written, so it was abandoned rather than shipped short. Narrow the filters — an identical retry fails identically.
- `503`: Service temporarily unavailable. `code` is one of: - `api_paused` (retryable) — The API is paused for a short while (an operator lever against abuse, rarely pulled). Nothing ran and nothing was charged. Retry the same request after Retry-After.
- `504`: The request exceeded its server-side time budget and was abandoned. Raised by us, below the socket timeout at the edge, so it arrives as JSON with a request_id. `code` is one of: - `assessment_timeout` (retryable) — Scoring exceeded its deadline. Retry; a warm route usually succeeds. - `screening_timeout` (retryable) — The screening exceeded its time budget (screeningComputeBudgetMs) and was abandoned before it could be stored. No unit is charged, and the Idempotency-Key is RELEASED — so retry with the SAME key; it is a clean attempt, not a replay. Deliberately raised below the socket timeout at the edge, so this arrives as JSON with a request_id rather than as a gateway 504 we never see.

---

[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)
