1. How BoaterOS syndicates
We push to Boat Trader through their native partner API for anything new or changed. If their API returns anything non-2xx, we fall back to their legacy webhook endpoint and retry with exponential backoff. The same pattern applies to YachtWorld, Boats.com, and every partner in the syndication namespace.
You can also register custom destinations — your own marketplace, a manufacturer co-op feed, an internal data warehouse. The transform pipeline is the same; only the endpoint and auth change.
2. Registering a custom destination
curl https://api.boater.os/v1/syndication/destinations \ -H "Authorization: Bearer $BOATEROS_KEY" \ -d '{ "name": "Boat Trader — North Fort Myers", "type": "boat_trader", "credentials": { "partner_id": "BT_84421", "secret_ref": "bt_prod_secret" }, "transform_id": "tf_default_boat_trader", "filter": { "location_ids": ["loc_ftmyers"], "status": ["available"] } }'
The filter is applied before transformation, so hulls that don't match never hit the partner. This is how you avoid accidentally syndicating consignments you don't have rights on.
3. Payload transform
A transform is a pure function: BoaterOS Hull in, partner payload out. The default one for Boat Trader ships with BoaterOS; override it for anything non-standard. Transforms run in a sandboxed V8 isolate with a 500ms CPU budget.
import type { Hull, BoatTraderPayload } from "@boateros/syndication"; export default function transform(hull: Hull): BoatTraderPayload { return { partner_ref: hull.stock_number, make: hull.make, model: hull.model, model_year: hull.year, length_ft: hull.length_ft, hin: hull.hin, price: hull.price_usd ? { amount: hull.price_usd * 100, currency: "USD" } : undefined, condition: hull.is_new ? "new" : "used", category: hull.category ?? "power", description: hull.description ?? buildDefaultDescription(hull), photos: hull.photos.slice(0, 40).map(p => ({ url: p.url, order: p.order })), dealer: { id: ctx.dealer.partner_ids.boat_trader, location: hull.location_id, }, }; }
4. Ack / nack protocol
Boat Trader's API returns one of three outcomes per push:
- ACK — 2xx +
listing_id. Stored on the hull assyndication.boat_trader.listing_id. - NACK — 4xx + one or more error codes. The push is marked failed with an error code; no retry unless you fix the root and republish.
- DEFER — 202 +
ticket_id. Their review queue. We poll/partners/v1/tickets/{id}every 15 min for up to 24h.
HTTP/1.1 422 Unprocessable Entity
{
"status": "nack",
"partner_ref": "STK-4402A",
"errors": [
{
"code": "IMG_TOO_SMALL",
"message": "Photo 3 is 640x480; minimum 800x600.",
"path": "photos[2].url"
}
],
"trace_id": "bt_trc_9F8A21D4"
} 5. Common rejection codes
6. The retry dashboard
Under Settings → Syndication → Retry. Every NACK shows up with the partner's exact error message, the payload we sent, and a one-click "fix and republish" for the common cases. Bulk-fix is available for code classes — e.g. "re-upload photo originals for all IMG_TOO_SMALL rejections from March."
Note. Republish rate limiting is strict — Boat Trader will block a dealer account that churns listings. We throttle automatic republishes to once per hull per 6 hours and surface a warning if you're close to the limit.