{
  "openapi": "3.1.0",
  "info": {
    "title": "Jet Cab AMS booking API",
    "version": "1.1.0",
    "description": "Two endpoints: a side-effect-free quote and the booking request. Booking creates a stored request and sends email; tours and custom rides also return a Mollie checkout URL for a 25% deposit. Availability is never guaranteed by an API response: the driver confirms fixed rides by email, and tours/custom rides are final after payment. Do not call POST /api/book without the customer's explicit approval of the shown quote and acceptance of the terms (https://www.jetcabams.nl/terms.html). Rate limits: 5 booking requests and 30 quotes per client address per 15 minutes; HTTP 429 with Retry-After when exceeded. Every booking requires the signed quoteToken from /api/quote. Quotes expire after 15 minutes; expiry, changed rules or changed journeys return HTTP 409 reviewRequired=true, requiring a fresh quote and renewed customer approval. Custom fares are honored within the validity window, never silently repriced at checkout. Tour pickup is required and at most five tours may be selected."
  },
  "servers": [
    {
      "url": "https://www.jetcabams.nl"
    }
  ],
  "paths": {
    "/api/quote": {
      "post": {
        "operationId": "quote",
        "summary": "Check rules and price. No booking, email or payment is created; no personal details or terms acceptance required.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/QuoteRequest"
              },
              "examples": {
                "fixed": {
                  "summary": "Schiphol transfer, rules/price check only",
                  "value": {
                    "car": "eclass",
                    "date": "2026-10-08",
                    "time": "10:00",
                    "passengers": 2,
                    "type": "fixed",
                    "routeId": "ams-spl",
                    "direction": "to",
                    "address": "Dam 1, 1012 JS Amsterdam"
                  }
                },
                "custom": {
                  "summary": "Custom ride, rules/price check only",
                  "value": {
                    "car": "eclass",
                    "date": "2026-10-08",
                    "time": "10:00",
                    "passengers": 2,
                    "type": "custom",
                    "pickup": "Dam 1, 1012 JS Amsterdam",
                    "dropoff": "Stationsplein 9, 1012 AB Amsterdam"
                  }
                },
                "tour": {
                  "summary": "Private tour with agreed pickup, rules/price check only",
                  "value": {
                    "car": "eclass",
                    "date": "2026-10-08",
                    "time": "10:00",
                    "passengers": 2,
                    "type": "tour",
                    "tourIds": [
                      "amsterdam-city-tour"
                    ],
                    "pickup": "Hotel Okura, Ferdinand Bolstraat 333, Amsterdam"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Eligible under the pricing/time rules (not a guarantee of driver availability).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuoteResponse"
                }
              }
            }
          },
          "400": {
            "description": "Not eligible; error is a machine-readable reason.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited."
          },
          "502": {
            "description": "Custom-ride fare could not be calculated; retry later."
          },
          "503": {
            "description": "quote_unavailable: signing service not configured; no accepted quote is returned."
          }
        }
      }
    },
    "/api/book": {
      "post": {
        "operationId": "book",
        "summary": "Create a booking request (fixed) or start a deposit checkout (tour, custom). Has side effects: stores the request, emails the driver and customer, and may create a payable Mollie checkout.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BookRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "paymentRequired=false: request received, driver confirms by email. paymentRequired=true: send the customer to checkoutUrl to pay the deposit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BookResponse"
                }
              }
            }
          },
          "400": {
            "description": "Validation or rule error (see error codes).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Origin not allowed."
          },
          "429": {
            "description": "Rate limited."
          },
          "502": {
            "description": "request_failed, payment_failed or custom_estimate_failed. Retrying the identical request with the same bookingId is safe; after editing details use a new bookingId."
          },
          "503": {
            "description": "payment_unavailable or mail_unavailable: service not configured."
          },
          "409": {
            "description": "quote_required, quote_invalid, quote_expired, quote_changed or quote_mismatch: reviewRequired=true. No booking/mail/checkout is created. Obtain a new quote and renewed explicit customer approval, then retry with its token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "QuoteRequest": {
        "type": "object",
        "allOf": [
          {
            "$ref": "#/components/schemas/RideRequirements"
          }
        ],
        "required": [
          "type",
          "car",
          "date",
          "time",
          "passengers"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "fixed",
              "custom",
              "tour"
            ]
          },
          "car": {
            "type": "string",
            "enum": [
              "eclass",
              "sclass",
              "vclass"
            ],
            "description": "E-Class max 4, S-Class max 2, V-Class max 7 passengers."
          },
          "date": {
            "type": "string",
            "format": "date",
            "description": "Local date in Europe/Amsterdam."
          },
          "time": {
            "type": "string",
            "pattern": "^\\d{2}:\\d{2}$",
            "description": "Local time in Europe/Amsterdam, between 04:00 and 20:00."
          },
          "passengers": {
            "type": "integer",
            "minimum": 1,
            "maximum": 7
          },
          "luggageLarge": {
            "type": "integer",
            "minimum": 0,
            "maximum": 8,
            "default": 0,
            "description": "Large check-in suitcases. E-Class and S-Class take at most 2; more needs the V-Class (error code luggage)."
          },
          "luggageSmall": {
            "type": "integer",
            "minimum": 0,
            "maximum": 8,
            "default": 0,
            "description": "Pieces of hand luggage. E-Class and S-Class take at most 2; more needs the V-Class (error code luggage)."
          },
          "returnTrip": {
            "type": "object",
            "description": "Fixed rides only. Books the return leg as well: the opposite direction between the same address and Schiphol, with the same vehicle, passengers and luggage. It is priced as a second ride (plus the name-card fee when meetGreet is set on a return that starts at Schiphol). It must be at least one hour after the first ride, between 04:00 and 20:00, and not on a blocked date. Error codes: retour_type, retour, retour_datetime, retour_hours, retour_date_blocked, retour_order. The booking is stored as two linked bookings: the request id and `<id>-ret`.",
            "properties": {
              "date": {
                "type": "string",
                "format": "date"
              },
              "time": {
                "type": "string",
                "pattern": "^\\d{2}:\\d{2}$"
              },
              "flight": {
                "type": "string",
                "maxLength": 24
              },
              "meetGreet": {
                "type": "boolean",
                "default": false
              }
            },
            "required": [
              "date",
              "time"
            ]
          },
          "routeId": {
            "type": "string",
            "description": "Fixed routes only; ids from /data/rates.json. The server reprices to the route the address belongs to.",
            "enum": [
              "ams-spl",
              "noord-spl",
              "zaandam-spl",
              "aveen-spl",
              "haarlem-spl",
              "zandvoort-spl",
              "dh-spl",
              "rtm-spl",
              "almere-spl",
              "almere-buiten-spl",
              "utrecht-spl"
            ]
          },
          "direction": {
            "type": "string",
            "enum": [
              "to",
              "from"
            ],
            "description": "Fixed routes only: to or from Schiphol."
          },
          "address": {
            "type": "string",
            "minLength": 4,
            "maxLength": 180,
            "description": "Fixed routes only: the non-airport pickup/drop-off address. The fare follows the route zone of the address (postcode first, then town name). An address whose Dutch postcode is in no fixed-route zone is refused with error code address_zone; book it as a custom ride."
          },
          "meetGreet": {
            "type": "boolean",
            "description": "Optional €25 arrivals-area name card; only for direction=from."
          },
          "pickup": {
            "type": "string",
            "description": "Required for custom rides and tours: agreed pickup address or hotel, preserved in review and confirmations.",
            "minLength": 4,
            "maxLength": 180
          },
          "dropoff": {
            "type": "string",
            "description": "Required for custom rides: drop-off address.",
            "minLength": 4,
            "maxLength": 180
          },
          "tourIds": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "amsterdam-city-tour",
                "countryside-tour",
                "keukenhof-tour",
                "aalsmeer-flower-auction",
                "dutch-water-defence-line",
                "giethoorn-day-trip",
                "south-holland-tour",
                "rotterdam-kinderdijk",
                "kroller-muller",
                "efteling-day-trip",
                "bruges-day-trip",
                "roermond-outlet",
                "mall-of-the-netherlands",
                "batavia-stad",
                "style-outlet-amsterdam"
              ]
            },
            "minItems": 1,
            "maxItems": 5,
            "description": "Tours only; ids from /data/tours.json.",
            "uniqueItems": true
          },
          "ticketService": {
            "type": "boolean",
            "description": "Tours only: attraction ticket purchase assistance, requires 28 days' notice."
          },
          "driverName": {
            "type": "string",
            "maxLength": 60
          },
          "flight": {
            "type": "string",
            "maxLength": 24
          },
          "notes": {
            "type": "string",
            "maxLength": 1000
          },
          "lang": {
            "type": "string",
            "enum": [
              "en",
              "nl",
              "de",
              "fr",
              "es",
              "pt",
              "ru",
              "ar",
              "zh-CN"
            ]
          }
        }
      },
      "BookRequest": {
        "type": "object",
        "allOf": [
          {
            "$ref": "#/components/schemas/RideRequirements"
          }
        ],
        "required": [
          "bookingId",
          "type",
          "car",
          "date",
          "time",
          "passengers",
          "name",
          "email",
          "phone",
          "acceptedTerms",
          "quoteToken"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "fixed",
              "custom",
              "tour"
            ]
          },
          "car": {
            "type": "string",
            "enum": [
              "eclass",
              "sclass",
              "vclass"
            ],
            "description": "E-Class max 4, S-Class max 2, V-Class max 7 passengers."
          },
          "date": {
            "type": "string",
            "format": "date",
            "description": "Local date in Europe/Amsterdam."
          },
          "time": {
            "type": "string",
            "pattern": "^\\d{2}:\\d{2}$",
            "description": "Local time in Europe/Amsterdam, between 04:00 and 20:00."
          },
          "passengers": {
            "type": "integer",
            "minimum": 1,
            "maximum": 7
          },
          "luggageLarge": {
            "type": "integer",
            "minimum": 0,
            "maximum": 8,
            "default": 0,
            "description": "Large check-in suitcases. E-Class and S-Class take at most 2; more needs the V-Class (error code luggage)."
          },
          "luggageSmall": {
            "type": "integer",
            "minimum": 0,
            "maximum": 8,
            "default": 0,
            "description": "Pieces of hand luggage. E-Class and S-Class take at most 2; more needs the V-Class (error code luggage)."
          },
          "returnTrip": {
            "type": "object",
            "description": "Fixed rides only. Books the return leg as well: the opposite direction between the same address and Schiphol, with the same vehicle, passengers and luggage. It is priced as a second ride (plus the name-card fee when meetGreet is set on a return that starts at Schiphol). It must be at least one hour after the first ride, between 04:00 and 20:00, and not on a blocked date. Error codes: retour_type, retour, retour_datetime, retour_hours, retour_date_blocked, retour_order. The booking is stored as two linked bookings: the request id and `<id>-ret`.",
            "properties": {
              "date": {
                "type": "string",
                "format": "date"
              },
              "time": {
                "type": "string",
                "pattern": "^\\d{2}:\\d{2}$"
              },
              "flight": {
                "type": "string",
                "maxLength": 24
              },
              "meetGreet": {
                "type": "boolean",
                "default": false
              }
            },
            "required": [
              "date",
              "time"
            ]
          },
          "bundle": {
            "type": "array",
            "minItems": 1,
            "maxItems": 4,
            "items": {
              "$ref": "#/components/schemas/BookRequest"
            },
            "description": "More rides booked together with this one (up to 5 rides in total). Every ride is a complete request with its own bookingId and quoteToken and is validated and price-checked on its own; the error carries `rideIndex` (0 = the outer request) when one is refused. Tours and custom rides in the request share ONE Mollie payment of the sum of each ride's 25% (`paymentRequired`: true with one `checkoutUrl`); fixed rides stay pay-in-vehicle requests. Each ride is stored as its own booking. Resending the same rides is idempotent; a different set of rides under the same booking ids is refused with `booking_conflict` (409). The response lists `bookingIds`."
          },
          "routeId": {
            "type": "string",
            "description": "Fixed routes only; ids from /data/rates.json. The server reprices to the route the address belongs to.",
            "enum": [
              "ams-spl",
              "noord-spl",
              "zaandam-spl",
              "aveen-spl",
              "haarlem-spl",
              "zandvoort-spl",
              "dh-spl",
              "rtm-spl",
              "almere-spl",
              "almere-buiten-spl",
              "utrecht-spl"
            ]
          },
          "direction": {
            "type": "string",
            "enum": [
              "to",
              "from"
            ],
            "description": "Fixed routes only: to or from Schiphol."
          },
          "address": {
            "type": "string",
            "minLength": 4,
            "maxLength": 180,
            "description": "Fixed routes only: the non-airport pickup/drop-off address. The fare follows the route zone of the address (postcode first, then town name). An address whose Dutch postcode is in no fixed-route zone is refused with error code address_zone; book it as a custom ride."
          },
          "meetGreet": {
            "type": "boolean",
            "description": "Optional €25 arrivals-area name card; only for direction=from."
          },
          "pickup": {
            "type": "string",
            "description": "Required for custom rides and tours: agreed pickup address or hotel, preserved in review and confirmations.",
            "minLength": 4,
            "maxLength": 180
          },
          "dropoff": {
            "type": "string",
            "description": "Required for custom rides: drop-off address.",
            "minLength": 4,
            "maxLength": 180
          },
          "tourIds": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "amsterdam-city-tour",
                "countryside-tour",
                "keukenhof-tour",
                "aalsmeer-flower-auction",
                "dutch-water-defence-line",
                "giethoorn-day-trip",
                "south-holland-tour",
                "rotterdam-kinderdijk",
                "kroller-muller",
                "efteling-day-trip",
                "bruges-day-trip",
                "roermond-outlet",
                "mall-of-the-netherlands",
                "batavia-stad",
                "style-outlet-amsterdam"
              ]
            },
            "minItems": 1,
            "maxItems": 5,
            "description": "Tours only; ids from /data/tours.json.",
            "uniqueItems": true
          },
          "ticketService": {
            "type": "boolean",
            "description": "Tours only: attraction ticket purchase assistance, requires 28 days' notice."
          },
          "driverName": {
            "type": "string",
            "maxLength": 60
          },
          "flight": {
            "type": "string",
            "maxLength": 24
          },
          "notes": {
            "type": "string",
            "maxLength": 1000
          },
          "lang": {
            "type": "string",
            "enum": [
              "en",
              "nl",
              "de",
              "fr",
              "es",
              "pt",
              "ru",
              "ar",
              "zh-CN"
            ]
          },
          "bookingId": {
            "type": "string",
            "pattern": "^[A-Za-z0-9_-]{8,80}$",
            "description": "Client-generated id. Re-sending the identical request with the same id is idempotent; a changed request needs a new id."
          },
          "quoteToken": {
            "type": "string",
            "minLength": 1,
            "maxLength": 2048,
            "description": "Signed token returned by POST /api/quote. Send unchanged only after the customer approves that quote's total and deposit. It binds the normalized journey, vehicle, passengers, options, fare, rules version and 15-minute expiry. HTTP 409 reviewRequired=true means obtain a new quote and renewed customer approval before retrying. Never supply your own price."
          },
          "name": {
            "type": "string",
            "minLength": 2,
            "maxLength": 100
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "phone": {
            "type": "string",
            "minLength": 7,
            "maxLength": 30
          },
          "company": {
            "type": "string",
            "description": "Honeypot for the website's hidden field. Must be omitted or empty; a non-empty value is treated as automated traffic and the request is acknowledged with suppressed:true without creating a booking, payment or email."
          },
          "acceptedTerms": {
            "type": "boolean",
            "const": true,
            "description": "Must be an explicit act by the customer after reading the Terms & Conditions. Agents must not set this on a customer's behalf without their consent."
          }
        }
      },
      "QuoteResponse": {
        "type": "object",
        "required": [
          "ok",
          "eligible",
          "quoteToken",
          "rulesVersion",
          "quotedAt",
          "validUntil",
          "quote"
        ],
        "properties": {
          "quoteToken": {
            "type": "string",
            "description": "Pass to POST /api/book after explicit customer review and approval. Not a booking or availability guarantee."
          },
          "ok": {
            "type": "boolean"
          },
          "eligible": {
            "type": "boolean"
          },
          "availability": {
            "const": "not_guaranteed"
          },
          "rulesVersion": {
            "type": "string"
          },
          "quotedAt": {
            "type": "string",
            "format": "date-time"
          },
          "validUntil": {
            "type": "string",
            "format": "date-time"
          },
          "timezone": {
            "const": "Europe/Amsterdam"
          },
          "currency": {
            "const": "EUR"
          },
          "vatIncluded": {
            "type": "boolean"
          },
          "quote": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string"
              },
              "car": {
                "type": "string"
              },
              "routeId": {
                "type": "string"
              },
              "routeLabel": {
                "type": "string"
              },
              "tourIds": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "basePrice": {
                "type": "number"
              },
              "meetGreetFee": {
                "type": "number"
              },
              "totalPrice": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "depositCents": {
                "type": "integer"
              },
              "payOnline": {
                "type": "string"
              },
              "payInVehicle": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "pickup": {
                "type": "string"
              },
              "dropoff": {
                "type": "string"
              },
              "address": {
                "type": "string"
              },
              "direction": {
                "type": "string"
              },
              "meetGreet": {
                "type": "boolean"
              },
              "ticketService": {
                "type": "boolean"
              }
            }
          }
        }
      },
      "BookResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "paymentRequired": {
            "type": "boolean"
          },
          "checkoutUrl": {
            "type": "string",
            "format": "uri"
          },
          "bookingId": {
            "type": "string"
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "reviewRequired": {
            "type": "boolean",
            "description": "On HTTP 409: no checkout or booking is created. Get a new quote and ask the customer to approve its total and deposit again."
          },
          "error": {
            "type": "string"
          },
          "bookingId": {
            "type": "string"
          }
        },
        "required": [
          "error"
        ],
        "description": "error is one of: address, body, body_too_large, booking_id, booking_id_conflict, capacity, car, content_type, custom_estimate_failed, date_blocked, date_range, datetime, direction, driver_name, dropoff, email, flight, hours, internal, lead_time, mail_unavailable, meet_greet_direction, method, name, notes, origin, payment_failed, payment_unavailable, phone, pickup, quote_required, quote_invalid, quote_expired, quote_changed, quote_mismatch, quote_unavailable, rate_limit, request_failed, route, terms, ticket_lead_time, tour, tour_limit, tour_duplicate, tour_price, tour_season, type."
      },
      "RideRequirements": {
        "type": "object",
        "properties": {
          "pickup": {
            "type": "string",
            "minLength": 4,
            "maxLength": 180,
            "description": "Required for custom rides and tours: agreed pickup address or hotel, preserved in review and confirmations."
          },
          "dropoff": {
            "type": "string",
            "minLength": 4,
            "maxLength": 180
          },
          "tourIds": {
            "type": "array",
            "uniqueItems": true
          },
          "email": {
            "type": "string",
            "maxLength": 254
          },
          "phone": {
            "type": "string",
            "pattern": "^[+()0-9 .-]{7,30}$"
          }
        },
        "allOf": [
          {
            "if": {
              "properties": {
                "type": {
                  "const": "fixed"
                }
              },
              "required": [
                "type"
              ]
            },
            "then": {
              "required": [
                "routeId",
                "direction",
                "address"
              ]
            }
          },
          {
            "if": {
              "properties": {
                "type": {
                  "const": "custom"
                }
              },
              "required": [
                "type"
              ]
            },
            "then": {
              "required": [
                "pickup",
                "dropoff"
              ]
            }
          },
          {
            "if": {
              "properties": {
                "type": {
                  "const": "tour"
                }
              },
              "required": [
                "type"
              ]
            },
            "then": {
              "required": [
                "tourIds",
                "pickup"
              ]
            }
          },
          {
            "if": {
              "properties": {
                "car": {
                  "const": "eclass"
                }
              },
              "required": [
                "car"
              ]
            },
            "then": {
              "properties": {
                "passengers": {
                  "maximum": 4
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "car": {
                  "const": "sclass"
                }
              },
              "required": [
                "car"
              ]
            },
            "then": {
              "properties": {
                "passengers": {
                  "maximum": 2
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "type": {
                  "const": "fixed"
                },
                "meetGreet": {
                  "const": true
                }
              },
              "required": [
                "type",
                "meetGreet"
              ]
            },
            "then": {
              "properties": {
                "direction": {
                  "const": "from"
                }
              }
            }
          }
        ]
      }
    }
  }
}
