Sync Maintenance to an ERP
Goal: every night, pull maintenance events completed in the last 24 hours and push them into an external ERP — so your finance system's maintenance-cost ledger never drifts from what OwlMar actually recorded.
Pattern: cursor-paginated list, filtered client-side by completedDate, run on a cron.
1. Sanity-check the endpoint
curl
curl -sS "https://api.owlmar.com/v1/maintenance/vessel/$VESSEL_ID?limit=25" \-H "Authorization: Bearer $OWLMAR_API_KEY"
json
{"data": [{"id": "9c2e1a04-...","eventType": "routine","status": "completed","completedDate": "2026-01-25T14:22:00.000Z","description": "500-hour service — port main engine","totalCost": "1249.50","currency": "USD"}],"pagination": { "cursor": "eyJpZCI6Ii4uLiJ9", "limit": 25, "hasMore": true, "total": 143 }}
2. The sync script
Node — nightly sync
import Decimal from 'decimal.js';const API_BASE = 'https://api.owlmar.com/v1';const API_KEY = process.env.OWLMAR_API_KEY;const VESSEL_IDS = process.env.OWLMAR_VESSEL_IDS.split(',');async function fetchApi(path) {const res = await fetch(API_BASE + path, {headers: { Authorization: 'Bearer ' + API_KEY },});if (res.status === 429) {const retryAfterMs = Number(res.headers.get('Retry-After') || 1) * 1000;await new Promise((resolve) => setTimeout(resolve, retryAfterMs));return fetchApi(path);}const body = await res.json();if (!res.ok) throw new Error(body.code + ': ' + body.error);return body;}async function fetchAllMaintenanceEvents(vesselId) {const events = [];let cursor = null;do {const qs = new URLSearchParams({ limit: '100' });if (cursor) qs.set('cursor', cursor);const { data, pagination } = await fetchApi('/maintenance/vessel/' + vesselId + '?' + qs.toString());events.push(...data);cursor = pagination.hasMore ? pagination.cursor : null;} while (cursor);return events;}async function syncVesselToErp(vesselId, sinceIso) {const events = await fetchAllMaintenanceEvents(vesselId);const completedSinceLastRun = events.filter((e) => e.status === 'completed' && e.completedDate && e.completedDate >= sinceIso);for (const event of completedSinceLastRun) {await pushToErp({externalId: event.id,vesselId,description: event.description,// totalCost is a Decimal-as-string — parse with a decimal library, never Number()totalCost: event.totalCost ? new Decimal(event.totalCost).toFixed(2) : null,currency: event.currency,completedAt: event.completedDate,});}return completedSinceLastRun.length;}async function main() {const sinceIso = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();let total = 0;for (const vesselId of VESSEL_IDS) {total += await syncVesselToErp(vesselId, sinceIso);}console.log('Synced ' + total + ' completed maintenance events to ERP.');}main();
cURL — page through cursor
curl -sS "https://api.owlmar.com/v1/maintenance/vessel/$VESSEL_ID?limit=100" \-H "Authorization: Bearer $OWLMAR_API_KEY"# Take .pagination.cursor from the response above and pass it back:curl -sS "https://api.owlmar.com/v1/maintenance/vessel/$VESSEL_ID?limit=100&cursor=$NEXT_CURSOR" \-H "Authorization: Bearer $OWLMAR_API_KEY"
Why it's built this way
- Cursor, not offset.
?page=on a growing table can skip or repeat rows if events are created between page fetches during the sync window.?cursor=doesn't have that failure mode — see Pagination. - Filter client-side by
completedDate. There's no server-sidecompletedSince=filter on this endpoint today — pulling the full result set and filtering locally is the correct approach at Enterprise's typical per-vessel maintenance volume (hundreds to low thousands of events, not millions). - Decimal costs parsed with a decimal library, not
Number()— see Response Envelope for why. - 429 handling inline — a nightly batch job is exactly the kind of caller that can safely
wait out a
Retry-Afterand continue, rather than failing the whole run.
Note
For a write-back direction (posting corrections from your ERP into OwlMar), use
PATCH /v1/maintenance/:id with an Idempotency-Key — see
Idempotency before wiring that up.
