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.
Quickstart #
Make your first lookup in under a minute.
-
1
Create an account
Sign up at app.marineos.io. The Free plan starts you with 10 lookups per month — no card required.
-
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
Make your first request
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.
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.
| Plan | Monthly lookups |
|---|---|
| Free | 10 |
| Technician | 500 |
| Business | 2,000 |
| Team | 10,000 |
| Enterprise | Unlimited |
- 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
/api/v1/health
No authLiveness check. Returns service status, version, and current timestamp. Useful for uptime monitors.
Response — 200
{
"status": "ok",
"service": "MarineOS API",
"version": "1.0.0",
"timestamp": "2026-05-05T17:42:18.219Z"
}
curl https://api.marineos.io/api/v1/healthconst 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"/api/v1/me
Auth requiredReturns the calling account, its plan, current usage against the monthly limit, and the API key being used.
Response — 200
{
"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
}
}
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"])/api/v1/lookup/:query
Auth requiredThe 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
| Param | Description |
|---|---|
| query | HIN (12 chars) or engine serial. Case-insensitive. Whitespace and dashes are stripped. |
Success — 200 (vessel)
{
"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)
{
"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
{
"error": "not_found",
"message": "No record matches the supplied identifier.",
"query": "ABC99999X999",
"meta": {
"remaining": 1585,
"plan": "business",
"requestId": "req_01HX8T7P0N3B0R"
}
}
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"])/api/v1/widget/lookup/:query
Auth + domainSame 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
{
"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"
}
}
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.
#!/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.
| Status | Code | Meaning |
|---|---|---|
| 400 | bad_request | Empty or malformed query parameter. |
| 401 | unauthorized | Missing X-API-Key header. |
| 403 | forbidden | Invalid or deactivated key, or request origin is not in the key's allowed domain. |
| 404 | not_found | The supplied identifier does not match any record in our database. |
| 429 | rate_limited | Monthly lookup quota exceeded. Upgrade your plan or wait until the cycle resets. |
| 500 | server_error | Unexpected server-side error. Retry with exponential backoff; contact support if persistent. |
Embeddable widget
Live · v1.0A 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.
<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.
| Attribute | Default | Description |
|---|---|---|
data-api-key | required | Your MarineOS API key. Use a domain-restricted key — keys are exposed in browser code. |
data-theme | light | light or dark. Switches the entire widget palette. |
data-accent | #0ea5e9 | Hex color for the CTA button, focus ring, and confidence bars. |
data-placeholder | Enter HIN or serial… | Override the input placeholder. |
data-cta | Look up | Override the submit button text. |
data-show-bom | true | Set to false to hide the bill-of-materials table. |
data-base-url | auto | Override 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.
<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.
// 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
allowedDomainin 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.