Charter Boats
Charter Boats API
Pricing

Resolve Pricing

Get the resolved price for each day in a date range

Same in API v1.0 and v1.1

GET/boats/:id/pricing

Resolve the effective daily price for each day in a date range. Each day is matched against the boat's seasonal pricing rules. Days with no season coverage come back with no price. This is a pricing lookup, not an availability check -- see the note under Response Fields.

Only sellable seasons take part: those of the boat's active pricing source (pricing_source on Get Boat — a boat listed on two platforms is priced from the one it is booked through), excluding quotes we can't sell (one-way delivery legs, weeks quoted from another base, implausible partner-side prices).

Authentication

Send your API key in the X-API-Key header, as on every Operator API call. It reads only boats your company operates; another operator's boat is a 403 — sell those through the Discovery API. See Authentication.

Path Parameters

ParameterTypeDescription
idstringBoat ID (UUID). A slug is not accepted

Query Parameters

ParameterTypeRequiredDescription
startstringYesStart date (YYYY-MM-DD)
endstringYesEnd date (YYYY-MM-DD)
productstringNoPrice one product variant (e.g. Bareboat, Crewed — see mmk_products on Get Boat). Seasons with no product stay in play. Without it, the seasons of every product compete for each day under the rules below, so pass it whenever the boat sells more than one package

The range runs from start up to but not including end — except on a day-clock boat (check-out later in the day than check-in), where the check-out day is a priced day and end is included (see totalDays and charter_unit below).

Request

curl "https://charter.boats/api/boats/3a121be0-c1f8-4432-8e18-4a125b46da65/pricing?start=2026-08-01&end=2026-08-08" \
  -H "X-API-Key: YOUR_API_KEY"

Response

{
  "prices": [
    {
      "date": "2026-08-01",
      "price": 213.31,
      "source": "season",
      "seasonName": "High Season 2026",
      "isPriced": true,
      "originalPrice": 355.5,
      "discountPercentage": 43,
      "discountName": "43% discount"
    },
    {
      "date": "2026-08-02",
      "price": 0,
      "source": "season",
      "isPriced": false
    },
    {
      "date": "2026-08-03",
      "price": 0,
      "source": "season",
      "isPriced": false
    },
    {
      "date": "2026-08-04",
      "price": 0,
      "source": "season",
      "isPriced": false
    },
    {
      "date": "2026-08-05",
      "price": 0,
      "source": "season",
      "isPriced": false
    },
    {
      "date": "2026-08-06",
      "price": 0,
      "source": "season",
      "isPriced": false
    },
    {
      "date": "2026-08-07",
      "price": 0,
      "source": "season",
      "isPriced": false
    }
  ],
  "extras_processing_percent": 5,
  "summary": {
    "totalDays": 7,
    "charter_unit": "night",
    "pricedDays": 1,
    "unpricedDays": 6,
    "subtotal": 213.31,
    "minPrice": 213.31,
    "maxPrice": 213.31,
    "avgPrice": 213.31
  }
}

Response Fields

prices array

FieldTypeDescription
datestringDate (YYYY-MM-DD)
pricenumberResolved price for this day; 0 when no season covers it (isPriced: false)
sourcestringAlways season
seasonNamestringName of the matching season (only present for priced days)
isPricedbooleanA season covers this date, so it has a price. Not an availability check -- see the note below
originalPricenumber | nullPre-discount price (if a seasonal discount applies)
discountPercentagenumber | nullActive discount percentage
discountNamestring | nullName of the discount

Top level

FieldTypeDescription
extras_processing_percentnumberPercentage added to our sale price to cover card processing on the required extras we collect at checkout (calculation_type: advance_payment, or none). Commission is charged on the charter price, so an extra we collect on the operator's behalf earns nothing while the card fees on it are ours; this covers them. The extras themselves pass through to the operator at face value, it is absorbed into the charter price rather than billed as a line, and it applies to no extra settled at the marina and to no optional extra — an operator who would rather not carry it marks the extra separate_payment and collects it at the base. Added 2026-09-19

summary object

FieldTypeDescription
totalDaysintegerCharter days priced — the stay's count in charter_unit: its nights on a night boat, its nights + 1 on a day-clock boat (check-out later in the day than check-in, so the check-out day is a priced day)
charter_unitstringnight, or day on a boat whose check-out is later in the day than its check-in — the unit totalDays counts in, so a 5→12 Oct stay is totalDays: 8, charter_unit: "day" there and 7, "night" elsewhere. Added 2026-09-24
pricedDaysintegerNumber of days a season covers
unpricedDaysintegerNumber of days no season covers
subtotalnumberSum of prices for priced days
minPricenumberLowest daily price among priced days
maxPricenumberHighest daily price among priced days
avgPricenumberAverage daily price among priced days

subtotal is the charter alone. It does not include the extras processing uplift above, the boat's own fees, or the flat platform fee — a booking total is charter + extras_processing_percent of the required extras billed at checkout + those extras + any optional extras chosen + the platform fee.

This endpoint does not check availability. It reads seasonal pricing only -- it never looks at bookings, blocks or synced calendar events, so a fully booked week still comes back with every day priced. To find out whether a boat is free, call GET /api/boats/availability and treat its blockedDates as nights: the checkout day is handed back in the morning and is never occupied.

Renamed on 2026-08-05 from isAvailable / availableDays / unavailableDays, which read as an availability check and were used as one.

Price Sources

  • season: The day is priced by a seasonal pricing rule. The matching season's name is included in seasonName.

Pricing Waterfall

  1. Season -- a matching season sets the price for the day. Where several seasons cover the same day, they are resolved in this order:
    1. The day's own week. A season that starts on the week-start the day falls in, counted in 7-day steps from the request's start, wins. Partner feeds return one quote per bookable week, so a boat with two changeover days has two overlapping seasons across almost every night; this picks the one quoted for the week the day actually sits in. A 14-day range therefore takes days 8-14 from week two's own season, not week one's. A partner (MMK or Nausys) season also counts as that week's own when the week-start falls in its check-in window — from its first day to its last day less the length of the quote it was built from — because equal back-to-back partner quotes are stored as one season.
    2. Priority -- higher priority wins.
    3. Narrower range -- on equal priority, the season covering fewer days wins, since a week-scoped quote is a fresher figure than the season-wide one it overlaps.
  2. No coverage -- if no season covers the day it has no price (isPriced: false, price: 0, no seasonName)

A Nausys boat that sells stays shorter than a week is quoted per stay length: a range of exactly N nights (N under 7) takes every day from that length's own quote where one covers it, before any of the rules above, and otherwise falls back to the week's. A week's quote prices any stay of 7 nights or more.

Step 1.1 only breaks ties between seasons that already cover the day -- it never stretches a season or invents a price. A range longer than its own week's season falls through to the remaining rules for the days outside it.

Errors

StatusMessage
400Boat ID must be a UUID
400start and end dates are required
400Dates must be in YYYY-MM-DD format
404Boat not found
500Failed to fetch seasons

An id that is not UUID-shaped — a slug, for instance — answers 400 without touching the database; a well-formed UUID no boat carries answers 404. Both used to be 500 with Boat not found in the message, so an integration written before 2026-09-15 that keys on the message keeps working.

Use Cases

Calculate Booking Total

Price the stay, then add the required fees. Fees are costed per stay the way charter.boats costs them — a percentage fee applies once, to its base for the whole stay, multiplied only by guests — and they split into what is paid online with the booking and what is settled at the base.

const API = 'https://charter.boats/api'
const headers = { 'X-API-Key': process.env.CHARTER_BOATS_API_KEY }
const get = (path) => fetch(`${API}${path}`, { headers }).then((r) => r.json())
 
const range = `start=${checkIn}&end=${checkOut}&product=${encodeURIComponent(product)}`
const { summary, extras_processing_percent } = await get(`/boats/${boatId}/pricing?${range}`)
if (summary.unpricedDays > 0) throw new Error('Not sold for these dates')
 
const fees = await get(`/boats/${boatId}/fees?${range}&guests=${guests}`)
const nights = summary.totalDays // the stay's count in summary.charter_unit — 8 days on a day-clock boat
const weeks = Math.ceil(nights / 7)
const charter = summary.subtotal
 
// The bases we cannot put a figure on: the operator sets them and never tells us what they are,
// or they are net of VAT while we only hold the gross price. A percentage of one of these is a
// RATE to state, never an amount to bill — see "Percentage fees" on the Fees endpoint.
const UNPRICEABLE = [
  'DEFINED_IN_CUSTOM', 'AGENCY_PRICE',
  'PRICELIST_PRICE', 'PRICELIST_PRICE_WITHOUT_VAT', 'CLIENT_PRICE_WITHOUT_VAT',
]
 
function feeCost(f) {
  if (f.amount_is_percentage) {
    // A percentage charged per head has no honest reading — it would multiply the whole
    // charter's share by the crew — so it is a rate too, not a number.
    if (UNPRICEABLE.includes(f.percentage_base) || f.period.startsWith('per_person')) return null
    const base = f.percentage_base === 'DAILY_PRICE' ? charter / nights : charter
    return (base * f.amount) / 100
  }
  switch (f.period) {
    case 'per_day': return f.amount * nights
    case 'per_week': return f.amount * weeks
    case 'per_person': return f.amount * guests
    case 'per_person_per_day':
    case 'per_person_per_night': return f.amount * guests * nights
    case 'per_person_per_week': return f.amount * guests * weeks
    default: return f.amount // per_booking, one_way
  }
}
 
const required = fees.filter((f) => f.required && f.calculation_type !== 'included_in_price')
// A fee we cannot put a number on is settled at the base whatever its calculation_type says —
// we can only bill what we can compute — so it is split out FIRST and quoted as a rate.
const priced = required.filter((f) => feeCost(f) !== null)
const rateOnly = required.filter((f) => feeCost(f) === null)
const online = priced.filter((f) => f.calculation_type === 'advance_payment')
const atBase = priced.filter((f) => f.calculation_type !== 'advance_payment')
 
const SERVICE_FEE = 45 // flat €45 per booking
const onlineExtras = online.reduce((t, f) => t + feeCost(f), 0)
// Extras processing: our sale price carries `extras_processing_percent` of the extras we
// collect at checkout. It is part of the charter price we charge, not a line of its own.
const dueOnline = charter
  + (onlineExtras * extras_processing_percent) / 100
  + onlineExtras
  + SERVICE_FEE
const dueAtBase = atBase.reduce((t, f) => t + feeCost(f), 0)
// …plus whatever `rateOnly` states. Quote those as rates beside the total ("€336 + 10% of a base
// the operator sets"), never folded into it: a sum that silently drops them reads as a complete
// price and is not one.

Fees settled at the base (separate_payment, or no calculation_type) are real costs of the charter but are not part of the online payment. Confirm the dates are free with Boat Availability before presenting a total — this endpoint prices booked weeks too.

Display Calendar Prices

curl "https://charter.boats/api/boats/3a121be0-c1f8-4432-8e18-4a125b46da65/pricing?start=2026-07-01&end=2026-08-01" \
  -H "X-API-Key: YOUR_API_KEY"

On this page