---
name: wingo-marine
description: >
  Integrate Wingo Marine (marine.wingo.ro) — live AIS vessels, fleets, ports,
  credit-metered public API, and signed fleet webhooks. Use when building
  shipping/logistics apps, vessel tracking, port calls, or maritime alerts;
  when the user mentions Wingo Marine, AIS API, fleet webhooks, MMSI/IMO, or
  "conectează Wingo Marine".
---

# Wingo Marine — integration skill

**Product:** [Wingo Marine](https://marine.wingo.ro) — real-time global AIS monitoring  
**Built by:** [Freevox](https://freevox.ro) · part of [Wingo](https://wingo.ro)

Use this skill to wire an external app to Wingo Marine via:

1. **Public REST API** (pull, credit-metered, `X-API-Key`)
2. **Webhooks** (push, HMAC-signed fleet events)

Do **not** invent endpoints. Prefer live docs: `https://marine.wingo.ro/developer` (after login) and `https://marine.wingo.ro` public marketing. Canonical reference: `docs/API.md` in the Wingo Marine monorepo / this skill’s summary below.

---

## Base URLs

| Surface | URL |
|---|---|
| App / UI | `https://marine.wingo.ro` |
| Public API | `https://marine.wingo.ro/api/public/v1` |
| Session API (browser cookies) | `https://marine.wingo.ro/api/v1` — **not** for server-to-server |
| Auth | `https://marine.wingo.ro/api/auth/*` (better-auth, browser) |

---

## Auth — API keys

1. User creates an account on marine.wingo.ro  
2. Subscribes (for fleet mutations / full UI tracks as needed) and buys credits  
3. **Developer → API** (`/developer`): create key → shown **once**  
4. Every public request:

```http
X-API-Key: ais_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

| Response | Meaning |
|---|---|
| 401 | Missing / invalid / revoked key |
| 402 | Insufficient credits (`required`, `remaining` in body) |
| 403 | Banned account or subscription required for fleet mutations |
| 429 | Rate limit (default 10 req/s per key) |

Success responses include:

- `X-Credits-Cost`
- `X-Credits-Remaining`

Store the key in env / secret manager. Never commit it. Never put it in a frontend SPA without a BFF.

---

## Identifiers

- **Vessels:** MMSI (9 digits) or IMO (7 digits) only — never internal DB ids  
- **Ports:** UN/LOCODE (5 alphanumerics)  
- Resolve by length: 9 → MMSI, 7 → IMO  

---

## Public API (pull)

Base: `https://marine.wingo.ro/api/public/v1`

| Method & path | Purpose | Default credits |
|---|---|---|
| `GET /vessels?search=&page=&pageSize=` | Search vessels | 2 |
| `GET /vessels/{mmsi\|imo}` | Vessel detail | 1 |
| `GET /vessels/{mmsi\|imo}/track?hours=` | Track (hours ≤ 168) | 5 |
| `GET /vessels/in-bbox?minLat=&minLon=&maxLat=&maxLon=` | Vessels in bbox (≤ 1000) | 5 |
| `GET /ports?search=&country=&page=` | Search ports | 1 |
| `GET /ports/{locode}` | Port detail | 1 |
| `GET /fleets` | Owner fleets | 1 |
| `GET /fleets/{id}` | Fleet + vessels | 1 |
| `GET /fleets/{id}/vessels` | Memberships only | 1 |
| `GET /fleets/{id}/positions` | All fleet positions | 2 |
| `POST /fleets/{id}/vessels` | Add vessel `{ "id", "label?" }` | 2 |
| `POST /fleets/{id}/vessels/bulk` | Bulk add `{ "ids": [] }` max 200 | 2 |
| `PATCH /fleets/{id}/vessels/{mmsi}` | Update label | 1 |
| `DELETE /fleets/{id}/vessels/{mmsi}` | Remove from fleet | 1 |

Fleet **mutations** require an **active subscription** on the key’s account. Lists are paginated: `{ items, total, page, pageSize }`.

### Example — TypeScript (server)

```ts
const BASE = 'https://marine.wingo.ro/api/public/v1';
const KEY = process.env.WINGO_MARINE_API_KEY!;

async function wmGet<T>(path: string): Promise<T> {
  const res = await fetch(`${BASE}${path}`, {
    headers: { 'X-API-Key': KEY },
  });
  if (!res.ok) {
    const body = await res.text();
    throw new Error(`Wingo Marine ${res.status}: ${body}`);
  }
  return res.json() as Promise<T>;
}

// Search
const page = await wmGet<{ items: unknown[]; total: number }>(
  '/vessels?search=maersk&page=1&pageSize=25',
);

// Detail by MMSI
const vessel = await wmGet(`/vessels/244660920`);

// Fleet live positions
const positions = await wmGet(`/fleets/3/positions`);
```

### Example — curl

```bash
curl -sS -H "X-API-Key: $WINGO_MARINE_API_KEY" \
  "https://marine.wingo.ro/api/public/v1/vessels?search=maersk"
```

---

## Webhooks (push)

Managed in UI: **https://marine.wingo.ro/webhooks** (session).  
Each endpoint has a **signing secret** (`whsec_…`) shown once at create/rotate.

### Events (fleet watch set only)

| Event | When |
|---|---|
| `fleet.position` | Sampled position of a watched vessel |
| `fleet.port_call` | Enter/exit port geofence (`phase: enter\|exit`) |
| `fleet.gone_silent` | No AIS reports past threshold |
| `fleet.destination_changed` | AIS destination changed |

Optional: restrict an endpoint to specific `fleetIds`; empty/null = all of the owner’s fleets.

### Delivery headers

```
Content-Type:        application/json
X-AIS-Event:         fleet.position
X-AIS-Timestamp:     1752745920000
X-AIS-Signature:     <hex HMAC-SHA256>
Idempotency-Key:     <delivery id>
X-AIS-Delivery-Id:   <delivery id>
User-Agent:          Wingo-Marine-Webhooks/2.0
```

### Verify signature (Node)

```ts
import crypto from 'crypto';

function verifyWingoWebhook(
  rawBody: Buffer | string,
  headers: { timestamp: string; signature: string },
  secret: string, // whsec_…
  maxSkewMs = 5 * 60 * 1000,
): boolean {
  const ts = Number(headers.timestamp);
  if (!Number.isFinite(ts) || Math.abs(Date.now() - ts) > maxSkewMs) return false;
  const body = typeof rawBody === 'string' ? rawBody : rawBody.toString('utf8');
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${ts}.${body}`)
    .digest('hex');
  const a = Buffer.from(expected, 'utf8');
  const b = Buffer.from(headers.signature, 'utf8');
  if (a.length !== b.length) return false;
  return crypto.timingSafeEqual(a, b);
}

// Express: app.post('/hooks/wingo', express.raw({ type: 'application/json' }), ...)
// Use raw body for HMAC — do not re-JSON.stringify.
```

### Payload shape (position)

```json
{
  "event": "fleet.position",
  "vessel": {
    "mmsi": 244660920,
    "lat": 51.9, "lon": 4.1,
    "sog": 12.4, "cog": 88.1, "heading": 90,
    "navStatus": 0, "destination": null,
    "positionTs": "2026-07-18T12:00:00.000Z"
  },
  "fleets": [{ "id": 3, "name": "North Sea" }],
  "occurredAt": "2026-07-18T12:00:01.000Z"
}
```

### Consumer checklist

1. HTTPS endpoint, respond **2xx quickly** (do heavy work async)  
2. Verify HMAC with **raw** body + `X-AIS-Timestamp`  
3. Reject if timestamp skew > 5 minutes  
4. Dedupe on `Idempotency-Key` / `X-AIS-Delivery-Id`  
5. Retries: exponential backoff (up to 5 attempts); after many failures the endpoint auto-disables  

---

## Recommended integration patterns

| Goal | Prefer |
|---|---|
| Occasional vessel lookup / search | Public API |
| Always-on map of “my ships” | Fleets + `GET …/positions` or webhooks `fleet.position` |
| Port enter/exit automation | Webhook `fleet.port_call` |
| Silence / missing AIS | Webhook `fleet.gone_silent` |
| Destination changes | Webhook `fleet.destination_changed` |
| Bulk offline analysis | Not for raw AIS dump — use API tracks or contact Freevox |

**Typical flow for a logistics app:**

1. Create fleet(s) in Wingo Marine UI or via public fleet APIs  
2. Add MMSIs/IMOs (API bulk or UI)  
3. Register webhook for `port_call` + `gone_silent` (+ optional `position`)  
4. Implement signed receiver; store delivery id for idempotency  
5. Use API for on-demand detail/track when user opens a vessel screen  

---

## Env template for client apps

```bash
WINGO_MARINE_API_BASE=https://marine.wingo.ro/api/public/v1
WINGO_MARINE_API_KEY=ais_...
WINGO_MARINE_WEBHOOK_SECRET=whsec_...
```

---

## Implementation rules for the agent

1. **Server-side only** for the API key and webhook secret.  
2. Always handle **402** and **429** with clear UX (top-up / backoff).  
3. Never hardcode MMSI as string without validation (`/^\d{9}$/` or `/^\d{7}$/` for IMO).  
4. When adding webhooks, implement **signature verification** before trusting the body.  
5. Prefer incremental integration: API read-only first, then fleets, then webhooks.  
6. Point humans to **https://marine.wingo.ro/developer** for live credit costs and key management.  
7. Branding in user-facing copy: **Wingo Marine** (not “AIS Monitor”).  

---

## Support / product links

- App: https://marine.wingo.ro  
- Freevox: https://freevox.ro  
- Wingo: https://wingo.ro  
- Public skill URL (this file when deployed): https://marine.wingo.ro/skills/wingo-marine/SKILL.md  
