ArcNautical Port lookup API

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.

Operations 1

GET /api/v1/ports Find a port by name, country, or UN/LOCODE (no key) #

Work with this as data

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 Specification

arcnautical-com-port-lookup-api-openapi.yml Raw ↑
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/