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.
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 usemultipart/form-data. - Lists return
{ data, page, per_page, total, has_more }. Usepageandper_page(max 100). - Dates are ISO-8601 in UTC (
2026-10-25T18:00:00Z). Money is a number in USD. - Update endpoints accept
PATCH(orPOSTif your client can't send PATCH) and change only the fields you send. GET /referencereturns every enum: boat types, states, statuses, fuel types, webhook events.
Listings
GET /listings | Your listings. Filters: status (comma list), sale_type, external_id, stock_number, updated_since, q, sort (updated|created|price), include=photos. |
|---|---|
POST /listings | Create. 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}/photos | Ordered photos with full and thumbnail URLs. |
|---|---|
POST /listings/{id}/photos | Either 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 /leads | Filters: 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-inquiries | Filters: 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 /offers | Filters: 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}/accept | Accept 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-requests | Filters: 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}/complete | Mark an accepted booking as completed. |
Webhooks: rental_request.created and rental_request.updated.
Auctions
GET /auctions | Filters: 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}/cancel | Cancel 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.
Featured placement & Bump
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 /featured | Your 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}/feature | Body { "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}/stop | Stop renewal; the placement runs to ends_at. |
POST /listings/{id}/bump | Returns 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.