Keel & Hull

Dealer Pro

Keel & Hull API

A plain JSON REST API and signed webhooks so your inventory system, CRM and website stay in sync with your Keel & Hull listings, leads, offers, financing requests, auctions and analytics.

Get started Pricing

Authentication

Create a key in Dashboard → Developer & API. Keys are shown once and can be read-only or read-write. Send it as a bearer token on every request:

curl https://keelandhull.com/api/v1/me -H "Authorization: Bearer kh_live…"

Keys belong to the account, not the user, and can be revoked at any time. All requests must use HTTPS. Base URL: https://keelandhull.com/api/v1.

Conventions

  • Request bodies are JSON (Content-Type: application/json). Form-encoded bodies also work for simple fields. Photo uploads use multipart/form-data.
  • Lists return { data, page, per_page, total, has_more }. Use page and per_page (max 100).
  • Dates are ISO-8601 in UTC (2026-10-25T18:00:00Z). Money is a number in USD.
  • Update endpoints accept PATCH (or POST if your client can't send PATCH) and change only the fields you send.
  • GET /reference returns every enum: boat types, states, statuses, fuel types, webhook events.

Listings

GET /listingsYour listings. Filters: status (comma list), sale_type, external_id, stock_number, updated_since, q, sort (updated|created|price), include=photos.
POST /listingsCreate. Required: make, model. Defaults: status: draft, condition: used, sale_type: sale. Send status: active to publish immediately.
GET /listings/{id}One listing with photos.
PATCH /listings/{id}Update any field below, including status (draft, active, pending, sold, archived; sold_price optional when marking sold).
DELETE /listings/{id}Archive (soft delete). Not allowed while an auction is open.

Fields: external_id (your DMS id — use it to find records later), stock_number, hin, title (auto-built from year/make/model when blank), year, make, model, boat_class, condition, length_ft, beam_ft, draft_ft, price, engine_make, engine_model, engine_count, engine_hours, horsepower, fuel_type, hull_material, description, location_id (from GET /locations) or city/state/zip, and financing: financing_available, financing_apr_from, financing_term_months, financing_down_pct, financing_lender, financing_notes; Best Offer: offers_enabled (default true on fixed-price listings), min_offer (private auto-decline floor); incentives — an array of up to 8 { "kind", "title", "details", "value_amount", "expires_on" } (kinds from GET /reference: delivery, trailer, storage, warranty, service, fuel, slip, training, detailing, electronics, safety, discount, financing_promo, trade_in, survey, fees, custom) shown on the listing and used by the "Incentives" search filter; and for sale_type: rental a rental object (see Rentals).

POST /api/v1/listings
{
  "external_id": "DMS-88213", "stock_number": "GW-1182",
  "year": 2021, "make": "Grady-White", "model": "Freedom 235",
  "boat_class": "Dual Console", "condition": "used", "length_ft": 23.5,
  "price": 129500, "engine_make": "Yamaha", "engine_model": "F300", "engine_count": 1,
  "engine_hours": 180, "horsepower": 300, "fuel_type": "gas",
  "description": "One owner, freshwater, full service history.",
  "financing_available": true, "financing_apr_from": 7.49, "financing_term_months": 180,
  "status": "active"
}

Photos

GET /listings/{id}/photosOrdered photos with full and thumbnail URLs.
POST /listings/{id}/photosEither JSON { "urls": ["https://…jpg", …] } (we fetch, resize to 1600px + 480px thumbnails) or multipart files in the photos field. JPG, PNG, WebP, HEIC. Max 40 per listing, 15 MB each.
PUT /listings/{id}/photos/order{ "order": [photoId, …] } — first id becomes the cover.
DELETE /listings/{id}/photos/{photoId}Remove a photo.

AI drafts

POST /listings/{id}/ai-draft returns a proposed title, description, type and specs built from the listing's HIN, your fields and its photos. Nothing is saved — review it, then PATCH the fields you want. Optional body fields (hin, make, model, year…) override what's on the listing. Limited to 25 drafts per account per day; returns 503 ai_unavailable when the feature is off.

Leads

GET /leadsFilters: status (new, contacted, closed, spam), listing_id, source (web, viewing, financing, rental), since.
GET /leads/{id}One lead; includes financing when it came from a financing request.
PATCH /leads/{id}{ "status": "contacted" } — records your first-response time.

Financing requests

Buyers on listings with financing_available can send a financing request. You receive contact details and preferences only — down payment, preferred term, self-reported credit range, employment type, co-applicant, trade-in, timeframe. Keel & Hull never collects SSNs, dates of birth, income figures or bank details and runs no credit checks; the application happens with you or your lender.

GET /financing-inquiriesFilters: status, listing_id, since.
GET /financing-inquiries/{id}One request.
PATCH /financing-inquiries/{id}{ "status": "contacted" }

Offers

Best Offers on your fixed-price listings. Each move resets a 72-hour clock; up to 8 rounds.

GET /offersFilters: status (open, accepted, declined, expired, withdrawn), listing_id, awaiting=seller for the ones that need you.
GET /offers/{id}Offer with full history.
POST /offers/{id}/acceptAccept the buyer's current number. Marks the listing sale pending and declines other open offers on it.
POST /offers/{id}/counter{ "amount": 118000, "message": "…" }
POST /offers/{id}/decline{ "message": "…" } (optional)

Rentals

List a boat for rent with sale_type: "rental" and a rental object; the headline rate is mirrored into price and the listing appears under /rentals. Renters send booking requests from the listing; you (or your system) accept, decline or complete them. Keel & Hull never takes payment.

rental fields: pricing_mode (boat | person), rates price_hour, price_half_day, price_day, price_week, price_trip, price_person, trip_hours, trip_description, min_hours, min_days, max_days, min_guests, max_guests, captain (included | optional | bareboat), captain_fee, license_required, fuel (included | pay_per_use | refill), deposit, cleaning_fee, season_start, season_end, days_available (array of mon…sun), earliest_start, latest_return, pickup, delivery_available, delivery_radius_mi, delivery_fee, amenities (array; keys from GET /reference), rules, cancellation (flexible | moderate | strict), notes, instant_book.

POST /api/v1/listings
{
  "sale_type": "rental", "year": 2022, "make": "Bennington", "model": "22 SSX", "boat_class": "Pontoon",
  "length_ft": 22, "city": "Fort Lauderdale", "state": "FL", "status": "active",
  "rental": { "pricing_mode": "boat", "price_half_day": 350, "price_day": 550, "price_week": 2900,
              "max_guests": 10, "captain": "optional", "captain_fee": 250, "fuel": "pay_per_use", "deposit": 500,
              "days_available": ["fri", "sat", "sun"], "pickup": "Bahia Mar Marina, slip B-12",
              "amenities": ["bimini", "bluetooth", "cooler", "safety_gear"], "cancellation": "moderate" }
}
GET /rental-requestsFilters: status (new, accepted, declined, expired, cancelled, completed), listing_id, from / to (start date range).
GET /rental-requests/{id}One request: renter, dates, duration_type, units, guests, estimate (from your rates), message.
POST /rental-requests/{id}/accept{ "note": "…" } (optional) — emails the renter your contact details.
POST /rental-requests/{id}/decline{ "note": "…" } (optional)
POST /rental-requests/{id}/completeMark an accepted booking as completed.

Webhooks: rental_request.created and rental_request.updated.

Auctions

GET /auctionsFilters: status, listing_id.
GET /auctions/{id}Auction with bid history and, once sold, the winner's contact details.
POST /auctions{ "listing_id", "starting_bid", "reserve_price"?, "buy_now_price"?, "duration_days" (3|5|7|10|14), "starts_at"? }. Omit starts_at to start now.
POST /auctions/{id}/cancelCancel a live or scheduled auction; the listing stays active as fixed price.

Analytics

GET /analytics?range=30&listing_id= — totals (views, leads, financing requests, bids, active listings), a daily series and a per-listing table for the last 7, 30, 90 or 365 days.

Paid add-ons. Featured puts a listing in the labelled band at the top of page one for a segment (a state or ALL × a boat type or ALL) for 30 days, renewing monthly until stopped. Bump sends any active listing back to the top of Newest with a short Best-match boost. Payment is finished in a browser via the returned Stripe Checkout URL; the API never takes card details. Organic ranking can't be bought.

GET /featuredYour placements with impressions, clicks, segment, renewal state, plus can_feature, slot and cap settings and the Bump price.
GET /featured/quote?state=FL&boat_class=Center%20Console&listing_id=123 → price for your plan, band (prime/standard), founding-rate flag, current share of voice and any reason the listing can't be featured there.
POST /listings/{id}/featureBody { "state": "FL", "boat_class": "Center Console" } (defaults to the listing's own). Returns 201 { purchase_id, checkout_url, amount }. The placement starts when the payment clears.
POST /featured/{id}/stopStop renewal; the placement runs to ends_at.
POST /listings/{id}/bumpReturns 201 { purchase_id, checkout_url, amount, days }. One bump per listing per bump period.

Webhooks

Register endpoints in the dashboard or via POST /webhooks { "url", "events"? }. Each delivery is a JSON POST:

POST https://your-endpoint
X-KH-Event: lead.created
X-KH-Delivery: 12345
X-KH-Signature: sha256=<hex HMAC-SHA256 of the raw body using your endpoint secret>

{ "id": "…", "event": "lead.created", "created_at": "2026-10-25T18:00:00Z",
  "data": { "lead_id": 88, "listing_id": 42, "name": "…", "email": "…", "phone": "…", "message": "…" } }

Events: ping lead.created financing_inquiry.created offer.created offer.updated offer.accepted auction.bid auction.closed listing.published listing.sold rental_request.created rental_request.updated . Respond with any 2xx within 10 seconds. Failures retry after 1, 5, 30, 120 and 720 minutes; an endpoint that fails 50 deliveries in a row is paused. POST /webhooks/{id}/ping sends a test.

Errors & limits

Errors are { "error": { "code", "message" } } with a matching HTTP status: 401 unauthorized, 403 plan_required / read_only_key, 404 not_found, 409 for state conflicts (e.g. auction_live), 422 invalid with the fields explained, 429 rate_limited. Limit: 1,000 requests per key per hour (X-RateLimit-Remaining header). Questions? Email the team.