Docs/Guides/Real-time inventory sync
◆ Guide

Real-time inventory sync to your website.

Webhook-first, polling as fallback, nightly reconciliation as a belt. The pattern we run on every marina site.

  • Six hull events with signed, versioned payloads
  • Idempotent by design — replays are safe
  • Next.js + TypeScript receiver sample
  • Reconciliation query for the 0.01% that slips
In this guide
  1. Architecture — webhooks vs. polling
  2. Registering a receiver
  3. The six hull events
  4. Idempotency and replays
  5. Sample Next.js API route
  6. Handling photos
  7. Polling as a fallback
  8. Daily reconciliation

1. Architecture

Two patterns work for keeping an external website in sync with BoaterOS inventory: push (webhooks) and pull (polling). We recommend webhooks as the primary path, with polling as a warm standby. Polling alone gets you median freshness of half your polling interval — usually minutes. Webhooks are p50 sub-second.

The belt-and-suspenders answer: subscribe to webhooks, and run a nightly full reconciliation that diffs your local inventory against GET /v1/hulls. We'll cover all three.

2. Registering a receiver

Your endpoint must respond 200 OK within 10 seconds — any 2xx counts. Don't do work in the request: 200 fast, then process async. We retry on any non-2xx with exponential backoff for 72 hours before dead-lettering.

terminal · curl
curl https://api.boater.os/v1/webhooks \
  -H "Authorization: Bearer $BOATEROS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourdealer.com/api/boateros/webhook",
    "events": ["hull.*"],
    "version": "2026-04-01"
  }'

The response contains a signing_secret. Store it. Every webhook is signed in the Boateros-Signature header as t=<unix>,v1=<hex> — same scheme as Stripe. Reject anything older than 5 minutes.

3. The six hull events

Event When it fires
hull.created A new hull was added. Includes full resource.
hull.updated Any field changed. Payload has before/after for changed fields only.
hull.price_changed Fires in addition to hull.updated. Price is load-bearing — handle it separately if you cache.
hull.status_changed available → on_hold → sold → delivered. Transitions are guaranteed in-order per hull_id.
hull.photos_updated Photos added, reordered, or removed. Full photos array in payload.
hull.archived Soft-deleted. You should remove it from your site within 5 minutes.

4. Idempotency

Every event carries an id (UUID v7) and a monotonic sequence per hull_id. Store both. If you've seen the id before, 200 and drop it. If the sequence you receive is lower than the one you last applied for that hull, 200 and drop it — it's an out-of-order replay.

This shows up when we retry after a 500: you may get the same event twice, and you may get it interleaved with a newer one. The sequence number resolves both cases.

5. Sample Next.js receiver

app/api/boateros/webhook/route.ts
import { NextRequest, NextResponse } from "next/server";
import { verify, queue, db } from "@/lib";

export async function POST(req: NextRequest) {
  const raw = await req.text();
  const sig = req.headers.get("boateros-signature") ?? "";

  if (!verify(raw, sig, process.env.BOATEROS_WEBHOOK_SECRET!)) {
    return new NextResponse("bad sig", { status: 401 });
  }
  const evt = JSON.parse(raw);

  // Idempotency: drop if we've seen this id or a newer sequence
  const seen = await db.events.upsert(evt.id, evt.data.id, evt.sequence);
  if (seen.stale) return new NextResponse("ok");

  await queue.publish("hull.sync", evt);   // process async
  return new NextResponse("ok");     // 200 in <50ms
}

6. Handling photos

Photos are delivered as CDN URLs, not binary. Don't re-host unless you have a reason — our CDN is cheaper and faster than yours. If you must mirror, handle hull.photos_updated specifically and fan out a transcode job per URL. URLs are immutable; a photo edit produces a new URL and a new event.

7. Polling fallback

If webhooks are down (your side or ours), cut over to polling with the updated_since parameter. It uses server-side update timestamps, not your clock.

curl
curl "https://api.boater.os/v1/hulls?updated_since=2026-04-22T10:00:00Z&limit=200" \
  -H "Authorization: Bearer $BOATEROS_KEY"

Paginate with the next cursor in the response envelope. 5 req/sec is the safe rate; our rate-limit headers tell you if you can go faster.

8. Daily reconciliation

Run a full pull once a day at 4am local. Compare it to your local mirror by id. Any hull you have but we don't → archive locally. Any hull we have but you don't → fetch and index. Any hull with a different updated_at → refetch. You'll find at most a handful per quarter; the reconciliation is there for the day you do.

◆ Next step

Keep your site current.

Webhooks are enabled on every plan. Grab a sandbox key, point it at ngrok, and ship this in a weekend.

Book a demo Back to docs