Every API here is available over the APIs.io API and to AI agents over MCP.
MCP server
One button, every client — Claude, Cursor, VS Code and the rest.
https://apis.io/mcp
Tools for apis
7 MCP tools reach this
find_apisBrowse and filter every API in the catalog.
get_api_artifactsOne API's artifacts, grouped by type.
get_openapiThe primary OpenAPI for this API.
find_similar_apisAPIs that look like this one.
apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
resolveTurn a domain, URL or GitHub org into the provider it belongs to.
find_cohortsEvery scored population of providers in the catalog.
All 92 tools →
Call it yourself
curl for this page
This API
curl "https://apis.io/api/v1/apis/arcnautical-com:arcnautical-com-port-lookup-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"
Discovery needs no key. Ratings and market analysis are Pro.
Get an API key
Free tier, no form to fill in. Signing in shares your email address with us — we
store it to create your key and to recognise you if you sign in with another
provider. See our Privacy Policy and
Terms.
A second provider on the same verified email joins the account you already have.
openapi: 3.2.0
info:
title: ArcNautical Port lookup API
version: v1
description: 'Vessel sanctions screening (single hull, batches of 50, standing monitors, fleets, cargo), voyage risk scoring, corridor monitoring, signed webhooks, exports and usage reporting. One keyless GET screens a hull; everything else runs on a self-serve key.
Limits and expiry are entitlement-specific. Use GET /api/v1/usage to read the authenticated key environment, quotas, batch limits, and live-monitor capacity.
The API exposes normalized route and risk outputs only. It does not expose raw third-party feeds, internal scoring weights, or AI-generated briefing prose.
FIELD NAMES ARE snake_case, AND UNKNOWN FIELDS ARE REFUSED. A body property this API does not read returns 422 with field_violations naming it and listing the accepted keys — it is never accepted and ignored. A camelCase spelling of a real field is named explicitly in the violation. Note that the keyless GET /api/v1/vessels/{imo}/check RESPONSE is camelCase; that is a different contract and does not carry over to request bodies here.
CORS: preflight (OPTIONS) returns Access-Control-Allow-Origin: * with Allow-Methods and an echo of your Access-Control-Request-Headers, so this API is callable from a browser. Authorization is not a CORS-safelisted header, so every cross-origin call — including GET — will preflight. Credentials are a bearer key you supply, never an ambient cookie, which is why * is both correct and permitted.
HTTP conventions: HEAD is accepted anywhere GET is and returns the same headers with no body. A valid path reached with the wrong method returns 405 with an Allow header, never 404. Content-Type may be application/json, text/plain, or absent — text/plain is accepted deliberately because that is what fetch() sets for a string body — but a header describing a genuinely different format (form-encoded, multipart, XML) returns 415 rather than being parsed as JSON anyway. limit is validated, never clamped — 0, a negative, a fraction, or a value above the maximum returns 422 rather than being silently adjusted. A path under /api/vN for a version this service does not publish returns a JSON envelope with code unsupported_api_version, so it is distinguishable from the HTML a network layer would return.'
x-api-version: '2026-10-29'
contact:
name: ArcNautical
url: https://arcnautical.com/developers/
email: admin@arcnautical.com
termsOfService: https://arcnautical.com/terms.html
x-logo:
url: https://arcnautical.com/logo-512.png
altText: ArcNautical
servers:
- url: https://arcnautical.com
description: Production
- url: http://localhost:3001
description: Local development
tags:
- name: Port lookup
description: Unauthenticated UN/LOCODE resolution. Routes are addressed by LOCODE, so this is how a caller turns a port name into one. Every LOCODE validation error points here.
paths:
/api/v1/ports:
get:
operationId: searchPorts
summary: Find a port by name, country, or UN/LOCODE (no key)
description: Resolves a port name to the UN/LOCODE that route.origin and route.destination require. Requires NO API key. Covers the ports the routing graph can compute against, so a code returned here is guaranteed to be routable. An exact LOCODE match is returned first, which makes this also the way to confirm a code you already hold. Every result carries match_score and match_type so a caller can tell a confident resolution from a weak one programmatically rather than trusting position in the list.
tags:
- Port lookup
security: []
parameters:
- name: query
in: query
required: true
description: Port name, country, or UN/LOCODE. Minimum two characters, case-insensitive, accents and spacing ignored. A port's official local or former name finds it too (lisboa, chattogram, antwerpen, bombay); the result carries the name we use.
schema:
type: string
minLength: 2
example: rotterdam
- name: limit
in: query
required: false
description: Maximum ports to return.
schema:
type: integer
minimum: 1
maximum: 100
default: 20
responses:
'200':
description: Matching ports, best match first. An empty array means no port matched.
headers:
Cache-Control:
description: Static reference data, cached for ten minutes.
schema:
type: string
content:
application/json:
schema:
$ref: '#/components/schemas/PortSearchResponse'
'422':
description: Missing or too-short query, or a limit outside 1-100. Uses the standard ApiErrorEnvelope.
content:
application/json:
schema:
$ref: '#/components/schemas/ApiErrorEnvelope'
components:
schemas:
FieldViolation:
type: object
required:
- field
- message
properties:
field:
type: string
message:
type: string
PortSearchResponse:
type: object
description: Ports matching the query, best match first. Returned by the keyless GET /api/v1/ports.
required:
- ports
properties:
ports:
type: array
items:
$ref: '#/components/schemas/Port'
ApiErrorEnvelope:
type: object
required:
- code
- message
- request_id
properties:
code:
type: string
enum:
- missing_idempotency_key
- invalid_idempotency_key
- invalid_json
- unauthorized
- invalid_api_key
- quota_exhausted
- insufficient_scope
- entitlement_inactive
- ip_not_allowed
- records_need_rpsl
- monitoring_unavailable_in_test
- simulation_unavailable_in_live
- not_found
- unsupported_api_version
- method_not_allowed
- unsupported_media_type
- idempotency_conflict
- idempotency_in_progress
- export_not_ready
- delivery_in_flight
- endpoint_disabled
- endpoint_deleted
- monitor_limit_exceeded
- vessel_monitor_limit_exceeded
- vessel_monitor_exists
- crew_ship_exists
- crew_ship_limit_exceeded
- export_unavailable
- validation_failed
- route_not_found
- export_too_large
- webhook_url_blocked
- webhook_url_unresolvable
- rate_limit_exceeded
- concurrency_limit
- service_concurrency_limit
- export_concurrency_limit
- internal_error
- assessment_failed
- screening_failed
- export_failed
- export_row_count_grew
- api_paused
- assessment_timeout
- screening_timeout
description: 'Stable machine code. Branch on this, never on the message text, which we reword. Generated from the list the server throws from rather than restated here, because a hand-kept copy of a server-side vocabulary has drifted on us before. The HTTP status does NOT tell you whether to retry: 409 idempotency_in_progress resolves by waiting while 409 idempotency_conflict never resolves, and 500 export_row_count_grew fails identically on every retry. Retryability per code:
- `missing_idempotency_key` (400) — not retryable. A POST arrived with no Idempotency-Key header.
- `invalid_idempotency_key` (400) — not retryable. The Idempotency-Key was present but unusable (empty or over-long).
- `invalid_json` (400) — not retryable. The body did not parse as JSON.
- `unauthorized` (401) — not retryable. No credential was supplied.
- `invalid_api_key` (401) — not retryable. The key is unknown, revoked, expired, or its prefix does not match the environment.
- `quota_exhausted` (402) — not retryable. 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.
- `insufficient_scope` (403) — not retryable. The key does not hold the scope this endpoint requires.
- `entitlement_inactive` (403) — not retryable. The account has no active entitlement.
- `ip_not_allowed` (403) — not retryable. The key is IP-restricted and this source address is not on its list.
- `records_need_rpsl` (403) — not retryable. The crew check PDF record comes with an RPSL licence linked to the account in the app. The check, its sha256 and verify_url are available without one.
- `monitoring_unavailable_in_test` (403) — not retryable. Vessel and corridor monitors are not available on a test key.
- `simulation_unavailable_in_live` (403) — not retryable. 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.
- `not_found` (404) — not retryable. No such resource, or it is not yours. A malformed id answers this way too, deliberately.
- `unsupported_api_version` (404) — not retryable. 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.
- `method_not_allowed` (405) — not retryable. The path exists but not with this verb. The Allow header names the verbs that do work.
- `unsupported_media_type` (415) — not retryable. Content-Type declared something other than application/json. Omitting the header is accepted; declaring the wrong one is not.
- `idempotency_conflict` (409) — not retryable. This Idempotency-Key was already used with a different body. Never resolves — use a new key.
- `idempotency_in_progress` (409) — 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` (409) — RETRYABLE. The export job has not finished. Poll it; carries Retry-After.
- `delivery_in_flight` (409) — RETRYABLE. That webhook delivery is being attempted right now.
- `endpoint_disabled` (409) — not retryable. The webhook endpoint is disabled. Re-enable it first.
- `endpoint_deleted` (409) — not retryable. The webhook endpoint no longer exists.
- `monitor_limit_exceeded` (409) — not retryable. Corridor monitor ceiling reached. Delete one first.
- `vessel_monitor_limit_exceeded` (409) — not retryable. Vessel monitor ceiling reached. Delete one first.
- `vessel_monitor_exists` (409) — not retryable. A monitor already watches this hull.
- `crew_ship_exists` (409) — not retryable. This IMO is already on your crew list; the message names its id.
- `crew_ship_limit_exceeded` (409) — not retryable. Crew list ceiling reached for this account (larger with an RPSL licence linked in the app). Delete one first.
- `export_unavailable` (410) — not retryable. The export file has aged out. The records behind it are unaffected — create a new one.
- `validation_failed` (422) — not retryable. One or more fields are invalid; field_violations names each one.
- `route_not_found` (422) — not retryable. Both ports are known but no sea route connects them.
- `export_too_large` (422) — not retryable. The requested range exceeds the per-export row cap. Narrow the filters and export in parts.
- `webhook_url_blocked` (422) — not retryable. The URL resolves to a private, loopback or link-local address.
- `webhook_url_unresolvable` (422) — not retryable. The URL host does not resolve.
- `rate_limit_exceeded` (429) — RETRYABLE. Over the per-key request rate. Shed RATE; back off to X-RateLimit-Reset.
- `concurrency_limit` (429) — 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` (429) — 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` (429) — RETRYABLE. Too many exports building at once. Wait for one to finish.
- `internal_error` (500) — RETRYABLE. An unhandled failure. Retry with backoff; quote request_id if it persists.
- `assessment_failed` (500) — RETRYABLE. Voyage scoring failed to produce a result.
- `screening_failed` (500) — RETRYABLE. The screening engine failed to produce a verdict. No unit is charged.
- `export_failed` (500) — RETRYABLE. The export job failed while building.
- `export_row_count_grew` (500) — not retryable. 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.
- `api_paused` (503) — 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.
- `assessment_timeout` (504) — RETRYABLE. Scoring exceeded its deadline. Retry; a warm route usually succeeds.
- `screening_timeout` (504) — 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.'
message:
type: string
request_id:
type: string
field_violations:
type: array
items:
$ref: '#/components/schemas/FieldViolation'
Port:
type: object
required:
- locode
- name
- country
- country_code
- lat
- lon
- match_score
- match_type
properties:
locode:
type: string
description: Five-character UN/LOCODE. This is the value route.origin and route.destination take.
name:
type: string
country:
type: string
country_code:
type: string
description: ISO 3166-1 alpha-2 country code.
lat:
type: number
lon:
type: number
match_score:
type: integer
minimum: 0
maximum: 100
description: 'How strong this match is, 0-100. Results are ordered by it. Publishing it means a caller can detect a weak resolution instead of taking the first row: a query that only matched the COUNTRY name scores 20, and auto-selecting that is almost always wrong.'
match_type:
type: string
enum:
- exact_locode
- exact_name
- name_prefix
- name_word
- locode_partial
- name_partial
- country
description: What matched. exact_locode (100) and exact_name (95) are safe to auto-select; country (20) means the query appeared in the country name only and should be shown to a human. exact_name, name_prefix and name_word also cover the port's official local and former names, so lisboa is an exact_name match on Lisbon (PTLIS).
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: arc_live_... or arc_test_...
description: 'Preferred authentication. Use Authorization: Bearer arc_live_... or arc_test_....'
legacyApiKey:
type: apiKey
in: header
name: X-ArcNautical-Key
description: Compatibility header for customers that cannot send Bearer tokens.
externalDocs:
description: Human quickstart, integration rules, and endpoint guide
url: https://arcnautical.com/developers/