API Reference · v1.0

The universal marine identifier API.

MarineOS resolves marine identifiers — Hull Identification Numbers (HINs) and engine serials — into structured vessel and powertrain data. One REST call, one source of truth across manufacturers, DMVs, and standards-based parsers. Built for marine retailers, insurance, surveyors, and inventory tooling.

Get an API key Read the quickstart Base URL https://api.marineos.io
HIN lookup
12-char hulls
Engine serials
All major OEMs
Provenance
Per-field source tier

Quickstart #

Make your first lookup in under a minute.

  1. 1

    Create an account

    Sign up at app.marineos.io. The Free plan starts you with 10 lookups per month — no card required.

  2. 2

    Generate an API key

    Open your dashboard and create a key. Keys are prefixed mos_live_. Optionally restrict the key to a single domain for browser use.

  3. 3

    Make your first request

    curl
    curl https://api.marineos.io/api/v1/lookup/BWC12345A404 \
      -H "X-API-Key: mos_live_REPLACE_WITH_YOUR_KEY"

Authentication #

All authenticated endpoints require an X-API-Key header.

Header
X-API-Key: mos_live_REPLACE_WITH_YOUR_KEY
  • Single-account scope. Each key is bound to one MarineOS account. Usage and rate limits are tracked against that account.
  • Domain restriction (optional). Configure an allowed domain in the dashboard to lock the key to a single origin — required when the key is exposed in browser code (see widget endpoint).
  • Rotation grace period. When you rotate a key, the old key continues to function for a short grace window so you can roll without downtime.
  • Never ship keys client-side unless they are domain-restricted and used only with the widget endpoint.

Rate limits #

Limits are tracked per calendar month and reset at the start of each billing cycle.

PlanMonthly lookups
Free10
Technician500
Business2,000
Team10,000
EnterpriseUnlimited
  • When the monthly quota is exceeded, the API returns 429 Too Many Requests.
  • Every successful response includes meta.remaining, the number of lookups left in the current cycle.
  • Need higher limits or burst pricing? Contact us about Enterprise.

Endpoints #

All endpoints are versioned under /api/v1. Responses are JSON.

Available routes

  • GET /api/v1/health No auth
  • GET /api/v1/me Auth required
  • GET /api/v1/lookup/:query Auth required
  • GET /api/v1/widget/lookup/:query Auth + domain
GET

/api/v1/health

No auth

Liveness check. Returns service status, version, and current timestamp. Useful for uptime monitors.

Response — 200

JSON
{
  "status": "ok",
  "service": "MarineOS API",
  "version": "1.0.0",
  "timestamp": "2026-05-05T17:42:18.219Z"
}
request
curl https://api.marineos.io/api/v1/health
const res = await fetch("https://api.marineos.io/api/v1/health");
const health = await res.json();
console.log(health.status); // "ok"
import requests

r = requests.get("https://api.marineos.io/api/v1/health")
print(r.json()["status"])  # "ok"
GET

/api/v1/me

Auth required

Returns the calling account, its plan, current usage against the monthly limit, and the API key being used.

Response — 200

JSON
{
  "account": {
    "id": "acc_8c2f9a",
    "email": "nick@example-marina.com",
    "name": "Nick Larsen",
    "company": "Example Marina",
    "plan": "business"
  },
  "usage": {
    "lookupCount": 412,
    "lookupLimit": 2000,
    "remaining": 1588
  },
  "apiKey": {
    "id": "key_3a91f0",
    "name": "Inventory backend",
    "allowedDomain": null
  }
}
request
curl https://api.marineos.io/api/v1/me \
  -H "X-API-Key: mos_live_REPLACE_WITH_YOUR_KEY"
const res = await fetch("https://api.marineos.io/api/v1/me", {
  headers: { "X-API-Key": "mos_live_REPLACE_WITH_YOUR_KEY" },
});
const me = await res.json();
console.log(me.usage.remaining);
import requests

r = requests.get(
    "https://api.marineos.io/api/v1/me",
    headers={"X-API-Key": "mos_live_REPLACE_WITH_YOUR_KEY"},
)
print(r.json()["usage"]["remaining"])
GET

/api/v1/lookup/:query

Auth required

The primary resolver. The path parameter :query accepts a 12-character HIN (e.g. BWC12345A404) or a manufacturer engine serial (e.g. 0R123456). MarineOS routes the query to the correct resolver and returns the unified record.

Path parameters

ParamDescription
queryHIN (12 chars) or engine serial. Case-insensitive. Whitespace and dashes are stripped.

Success — 200 (vessel)

JSON
{
  "type": "vessel",
  "query": "BWC12345A404",
  "vessel": {
    "hin": "BWC12345A404",
    "make": "Boston Whaler",
    "model": "Outrage 280",
    "year": 2014,
    "lengthFt": 28.0,
    "hullMaterial": "fiberglass"
  },
  "meta": {
    "remaining": 1587,
    "plan": "business",
    "requestId": "req_01HX8T7N2K4E3V"
  }
}

Success — 200 (engine)

JSON
{
  "type": "engine",
  "query": "0R123456",
  "engine": {
    "serial": "0R123456",
    "manufacturer": "Mercury",
    "model": "Verado 300",
    "horsepower": 300,
    "year": 2018
  },
  "meta": {
    "remaining": 1586,
    "plan": "business",
    "requestId": "req_01HX8T7N9P2A1Q"
  }
}

Not found — 404

JSON
{
  "error": "not_found",
  "message": "No record matches the supplied identifier.",
  "query": "ABC99999X999",
  "meta": {
    "remaining": 1585,
    "plan": "business",
    "requestId": "req_01HX8T7P0N3B0R"
  }
}
request
curl https://api.marineos.io/api/v1/lookup/BWC12345A404 \
  -H "X-API-Key: mos_live_REPLACE_WITH_YOUR_KEY"
const res = await fetch(
  "https://api.marineos.io/api/v1/lookup/BWC12345A404",
  { headers: { "X-API-Key": "mos_live_REPLACE_WITH_YOUR_KEY" } }
);

if (res.status === 404) {
  console.log("Not found");
} else if (res.status === 429) {
  console.log("Rate limited");
} else {
  const data = await res.json();
  console.log(data.vessel);
}
import requests

r = requests.get(
    "https://api.marineos.io/api/v1/lookup/BWC12345A404",
    headers={"X-API-Key": "mos_live_REPLACE_WITH_YOUR_KEY"},
)

if r.status_code == 404:
    print("Not found")
elif r.status_code == 429:
    print("Rate limited")
else:
    print(r.json()["vessel"])
GET

/api/v1/widget/lookup/:query

Auth + domain

Same resolver as /lookup but returns per-field confidence scores and provenance source tiers. Designed to back the embeddable retailer widget. Requires that the API key has an allowedDomain configured — requests from any other origin are rejected with 403.

Source tiers

  • manufacturer — direct OEM record (highest confidence)
  • dmv — state titling / registration database
  • parsed-hin — derived from the HIN itself per USCG standard
  • inferred — heuristic match based on adjacent records (lowest)

Success — 200

JSON
{
  "type": "vessel",
  "query": "BWC12345A404",
  "vessel": {
    "make":         { "value": "Boston Whaler", "confidence": 0.99, "source": "manufacturer" },
    "model":        { "value": "Outrage 280",   "confidence": 0.96, "source": "manufacturer" },
    "year":         { "value": 2014,            "confidence": 0.98, "source": "parsed-hin"  },
    "lengthFt":     { "value": 28.0,            "confidence": 0.92, "source": "manufacturer" },
    "hullMaterial": { "value": "fiberglass",    "confidence": 0.74, "source": "inferred"    }
  },
  "meta": {
    "remaining": 1584,
    "plan": "business",
    "requestId": "req_01HX8T7Q1M0C5D"
  }
}
request
curl https://api.marineos.io/api/v1/widget/lookup/BWC12345A404 \
  -H "X-API-Key: mos_live_REPLACE_WITH_YOUR_KEY" \
  -H "Origin: https://your-store.com"
// Use only with a domain-restricted key on the configured origin.
const res = await fetch(
  "https://api.marineos.io/api/v1/widget/lookup/BWC12345A404",
  { headers: { "X-API-Key": "mos_live_REPLACE_WITH_YOUR_KEY" } }
);
const data = await res.json();
console.log(data.vessel.make.confidence, data.vessel.make.source);
import requests

r = requests.get(
    "https://api.marineos.io/api/v1/widget/lookup/BWC12345A404",
    headers={
        "X-API-Key": "mos_live_REPLACE_WITH_YOUR_KEY",
        "Origin": "https://your-store.com",
    },
)
data = r.json()
print(data["vessel"]["make"]["confidence"], data["vessel"]["make"]["source"])

Code samples #

Drop-in clients you can adapt. Replace mos_live_REPLACE_WITH_YOUR_KEY with your real key. The base URL https://api.marineos.io is canonical; the direct Railway URL https://api-production-5d51.up.railway.app also works during DNS propagation.

marineos client
#!/usr/bin/env bash
set -euo pipefail

API_KEY="mos_live_REPLACE_WITH_YOUR_KEY"
BASE="https://api.marineos.io"
QUERY="${1:-BWC12345A404}"

curl -sS "$BASE/api/v1/lookup/$QUERY" \
  -H "X-API-Key: $API_KEY" | jq .
// MarineOS minimal client (Node 18+ / browsers with fetch)
const BASE = "https://api.marineos.io";

export async function marineosLookup(query, apiKey) {
  const res = await fetch(`${BASE}/api/v1/lookup/${encodeURIComponent(query)}`, {
    headers: { "X-API-Key": apiKey },
  });

  if (res.status === 404) return { found: false };
  if (res.status === 429) throw new Error("Rate limit exceeded");
  if (!res.ok) throw new Error(`MarineOS ${res.status}`);

  const body = await res.json();
  return { found: true, ...body };
}

// Usage
const r = await marineosLookup("BWC12345A404", "mos_live_REPLACE_WITH_YOUR_KEY");
console.log(r);
"""MarineOS minimal client."""
import requests

BASE = "https://api.marineos.io"


class MarineOSError(Exception):
    pass


def lookup(query: str, api_key: str) -> dict | None:
    r = requests.get(
        f"{BASE}/api/v1/lookup/{query}",
        headers={"X-API-Key": api_key},
        timeout=10,
    )
    if r.status_code == 404:
        return None
    if r.status_code == 429:
        raise MarineOSError("Rate limit exceeded")
    r.raise_for_status()
    return r.json()


if __name__ == "__main__":
    print(lookup("BWC12345A404", "mos_live_REPLACE_WITH_YOUR_KEY"))

Error codes #

All errors return a JSON body with error (machine-readable) and message (human-readable) fields.

StatusCodeMeaning
400bad_requestEmpty or malformed query parameter.
401unauthorizedMissing X-API-Key header.
403forbiddenInvalid or deactivated key, or request origin is not in the key's allowed domain.
404not_foundThe supplied identifier does not match any record in our database.
429rate_limitedMonthly lookup quota exceeded. Upgrade your plan or wait until the cycle resets.
500server_errorUnexpected server-side error. Retry with exponential backoff; contact support if persistent.

Embeddable widget

Live · v1.0

A drop-in HIN / serial lookup widget for retailer storefronts, parts catalogs, and quoting tools. Three lines, zero build step, ~14kb gzipped, isolated via Shadow DOM. See it live on a sample retailer site →

Install

Paste these two tags anywhere in your HTML — the widget mounts on every [data-marineos-widget] element on the page.

HTML
<script src="https://api.marineos.io/widget.js" async></script>
<div data-marineos-widget data-api-key="mos_live_REPLACE_WITH_YOUR_KEY"></div>

Configuration

All options are set via data-* attributes on the mount element. Only data-api-key is required.

AttributeDefaultDescription
data-api-keyrequiredYour MarineOS API key. Use a domain-restricted key — keys are exposed in browser code.
data-themelightlight or dark. Switches the entire widget palette.
data-accent#0ea5e9Hex color for the CTA button, focus ring, and confidence bars.
data-placeholderEnter HIN or serial…Override the input placeholder.
data-ctaLook upOverride the submit button text.
data-show-bomtrueSet to false to hide the bill-of-materials table.
data-base-urlautoOverride the API origin (advanced — for testing or self-hosted).

Themed example

Match your brand by setting data-theme and data-accent. Compact mode hides the BOM and shortens the CTA.

HTML
<div data-marineos-widget
     data-api-key="mos_live_..."
     data-theme="dark"
     data-accent="#f59e0b"
     data-show-bom="false"
     data-cta="Search"></div>

SPA & dynamic mounts

For React, Vue, or other single-page apps, call window.MarineOSWidget.mount(element, options) after the element is in the DOM. The loader auto-mounts on initial page load.

JavaScript
// After your component mounts
window.MarineOSWidget.mount(elementRef.current, {
  apiKey: 'mos_live_...',
  theme: 'light',
  accent: '#e63946',
  onResult: (data) => console.log('lookup result', data)
});

Notes

  • Domain-restrict your key. The widget exposes the API key in browser code — set allowedDomain in the dashboard so it can only be used from your origin(s).
  • Style isolation. The widget renders inside a Shadow DOM — your site's CSS will not bleed in.
  • Backed by /api/v1/widget/lookup/:query — per-field confidence scores and provenance tiers (manufacturer / dealer / crowdsourced).
  • Live demo: demo.marineos.io shows the widget on a sample retailer storefront with light, dark, and custom accent variants.

Support #

Talk to a human about Enterprise SLAs, custom integrations, or higher rate limits.