Read your boat's data with an API key, so the owner's office, a management company or tools like Zapier and Make can pull it. Get a signed webhook when something happens on board. Available on the Owner and Private Fleet plans.
A boat admin creates keys in the app under Settings > Integrations > API keys: a name, and an optional expiry (30 days, 90 days, a year, or never). The key is shown once; MySeaBase keeps only a SHA-256 fingerprint of it and cannot show it again. A boat can have up to 10 live keys. Revoking a key stops it at once. The list shows when each key was created and last used.
A key looks like this:
k.<boat code>.<secret> k.sea-breeze.Zb3kQ0x9V1mNw2... (the secret part is 43 characters)
Version 1 keys are read only. A key can read a fixed list of resources (below) and nothing else: it can never write, change users or settings, open files, or read private crew and guest data. Every call made with a key is written to the boat's audit log with the key's id.
Every call is one HTTPS POST to https://myseabase.com/api with a JSON body:
{"action": "<action name>", "args": [ ... ], "token": "<your API key>"}
The answer is always JSON: {"ok": true, "data": ...} or {"ok": false, "error": "..."}.
curl -s https://myseabase.com/api \
-H 'Content-Type: application/json' \
-d '{"action":"getKasaDurumu","args":[],"token":"k.sea-breeze.Zb3kQ0x9V1mNw2..."}'
{"ok": true, "data": [{"kasaAdi": "Boat EUR", "bakiyeler": [{"doviz": "EUR", "bakiye": 1840.5}]}]}
The same reads are also available as plain GET requests, with the key in the Authorization header (never in the address, so it does not end up in logs or browser history):
curl -s https://myseabase.com/api/v1/expenses?month=2027-01 \ -H 'Authorization: Bearer k.sea-breeze.Zb3kQ0x9V1mNw2...'
The answer has the same {"ok", "data"} shape, with an HTTP status that matches the error (see Errors).
| GET /api/v1/... | Query parameters | Action |
|---|---|---|
boat | none | getBoatProfile |
logbook | from, to (YYYY-MM-DD; default the last 30 days; at most 366 days) | listJurnalEntries |
expenses | status (submitted, approved, rejected, reimbursed), month (YYYY-MM), year, category, limit (1 to 200), before (id, for the next page) | listExpenses |
cash | none | getKasaDurumu |
fuel | from, to, kind (fuel, water), limit, before | listFuelLog |
maintenance | none | listMaintenance |
deadlines | days (1 to 366, default 90) | listUpcomingDeadlines |
defects | status (open, in_progress, waiting_parts, done, cancelled, active, closed), priority, area, limit, before | listDefects |
charter | range (upcoming, past, all, or YYYY-MM), status | listReservations |
crew | status (active, on_leave, left) | listCrew |
statistics | from, to (default the year to date, at most 3 years) | insightsReport |
owner-report | month (YYYY-MM, default this month) | ownerReport |
suppliers | filter (all, owed, overdue, credit), category, search | listSuppliers |
payables | status (all, open, overdue, due, submitted, paid, cancelled), supplier, category | listSupplierInvoices |
payables-summary | none | payablesSummary |
Lists that page (expenses, fuel, defects) return {"items": [...], "next": id or null}: pass next back as before for the following page.
With POST, args is a list in this order (leave trailing arguments out to use the defaults):
| Action | args | Returns |
|---|---|---|
getBoatProfile | [] | name, type, modules on, plan, subscription state |
listJurnalEntries | [from, to] | logbook entries, newest day first |
listExpenses | [{status, month, year, category}, limit, before] | expenses with amount, currency, category, vendor, status, receipt file ids |
getKasaDurumu | [] | cash balance per cash box and currency |
listFuelLog | [{from, to, kind}, limit, before] | fuel and water fills |
listMaintenance | [] | maintenance tasks with next due date or engine hours and status (ok, soon, overdue) |
listUpcomingDeadlines | [days] | the due list: maintenance, safety equipment, documents |
listDefects | [{status, priority, area, limit}, before] | defects |
listReservations | [range, status] | charter bookings: dates, party size, status, rate, deposit and balance |
listCrew | [status] | crew list: id, name, role, status, start date, contract end, certificate health |
insightsReport | [from, to] | statistics: days underway, outings, spend, fuel, places, charter, defects |
ownerReport | [month] | the owner's monthly report |
listSuppliers | [{filter, category, search}] | suppliers with balances, overdue and open invoices per currency, and totals (never bank details, contact or notes) |
listSupplierInvoices | [{status, supplier, category}] | supplier invoices with amount, paid, outstanding, overdue, next due and payment schedule (never notes or attachments) |
payablesSummary | [] | owed, overdue and due within a week per currency, by category, counts |
Each resource needs its module switched on for the boat; otherwise the answer is Module not enabled. Amounts are strings with two decimals, or integers in cents where the field name ends in Cents. Dates are YYYY-MM-DD; times are Unix seconds. Some field names are Turkish (for example kasaAdi, cash box name, and bakiye, balance) for historical reasons; they are stable.
Bu işlem için yetkiniz yok (no permission) before it runs.60 calls per key per rolling minute. Over that the answer is RATE_LIMITED (HTTP 429 on GET); wait a minute. There is also a per-address limit in front of the API (5 requests a second with short bursts). For a nightly sync, one call per resource is plenty.
| error | GET status | Meaning |
|---|---|---|
AUTH | 401 | missing, unknown, revoked or expired key |
Bu işlem için yetkiniz yok | 403 | the action is not readable with a key |
PLAN_REQUIRED | 403 | the boat is on the Crew plan; the API comes with Owner and Private Fleet |
BOAT_SUSPENDED | 403 | the boat's account is suspended |
Module not enabled | 403 | the boat does not use that module |
RATE_LIMITED | 429 | more than 60 calls in a minute with this key |
| a validation message | 400 | a bad argument, for example Geçersiz tarih: başlangıç (invalid start date) |
Bulunamadı | 404 | unknown GET resource |
With POST the HTTP status is always 200 and ok tells success from failure.
In Settings > Integrations > Webhooks a boat admin adds up to 5 HTTPS addresses and picks the events each one gets. MySeaBase then POSTs a small JSON message to the address when the event happens (checked about once a minute).
| Event | When | data |
|---|---|---|
expense.submitted | an expense is entered | id, date, amount, currency, category, status |
expense.approved | an expense is approved | same |
defect.opened | a defect is reported | id, title, priority, area, status |
defect.closed | a defect is done or cancelled | same |
maintenance.overdue | a maintenance task becomes overdue (once, until it is done) | id, title, nextDueDate, nextDueHours, hoursLeft |
charter.confirmed | a charter booking is confirmed | id, startDate, endDate, partySize, status |
logbook.saved | a logbook entry is saved | id, date |
safety.report_raised | a near miss, incident or non-conformity is reported (safety management module) | id, kind, occurredDate, status |
payable.created | a supplier invoice is recorded or accepted (expenses module) | id, date, dueDate, amount, currency, category, status, workOrderId |
payable.paid | a supplier invoice is fully paid | same |
ping | the Send test button | message |
The body never carries names of guests or crew, notes, vendors, pay or anything private: use the ids with the API if you need more.
POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: MySeaBase-Webhooks/1
X-MySeaBase-Event: expense.approved
X-MySeaBase-Delivery: 4182
X-MySeaBase-Timestamp: 1801234567
X-MySeaBase-Signature: sha256=5f0c...e91a
{"boat":"sea-breeze","data":{"amount":"120.00","category":"Marina","currency":"EUR","date":"2027-01-10","id":57,"status":"approved"},"event":"expense.approved","id":"evt_3f1c9a...","occurredAt":"2027-01-10T09:12:47Z"}
Answer with any 2xx status within 10 seconds. id is the same for every delivery of one event (use it to ignore a duplicate); X-MySeaBase-Delivery stays the same across retries of one delivery.
Addresses must be https:// and must resolve to public internet addresses only: private networks (10/8, 172.16/12, 192.168/16), loopback, link-local and the cloud metadata address are refused, when you save the address and again before every delivery. Redirects are not followed.
When you add a webhook the app shows its signing secret (whsec_...) once. The signature is HMAC-SHA256 over the timestamp, a dot and the raw body, with the secret as the key, in hex:
X-MySeaBase-Signature: sha256=hex(HMAC_SHA256(secret, timestamp + "." + raw_body))
Compare in constant time, against the raw bytes you received (not re-encoded JSON), and refuse a timestamp older than five minutes.
import hashlib, hmac, time
def verify(secret: str, headers: dict, raw_body: bytes) -> bool:
ts = headers.get("X-MySeaBase-Timestamp", "")
sig = headers.get("X-MySeaBase-Signature", "")
if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
return False
mac = hmac.new(secret.encode(), ts.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest("sha256=" + mac, sig)
const crypto = require('crypto');
function verify(secret, headers, rawBody /* Buffer */) {
const ts = headers['x-myseabase-timestamp'] || '';
const sig = headers['x-myseabase-signature'] || '';
if (!/^\d+$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const mac = crypto.createHmac('sha256', secret).update(ts + '.').update(rawBody).digest('hex');
const a = Buffer.from('sha256=' + mac), b = Buffer.from(sig);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Express: keep the raw body for the check
// app.post('/hook', express.raw({ type: 'application/json' }), (req, res) => {
// if (!verify(process.env.MYSEABASE_SECRET, req.headers, req.body)) return res.sendStatus(401);
// const event = JSON.parse(req.body); /* ... */ res.sendStatus(204);
// });
If your address does not answer with 2xx (an error status, a timeout or no connection), the delivery is tried again after 1 minute, 10 minutes and 1 hour, then given up. The Send test button sends one ping and does not retry. In the app each webhook has a Deliveries log with the last 50 deliveries: time, event, result, HTTP status and attempts.