{
  "openapi": "3.1.0",
  "info": {
    "title": "Up and Up Movers booking API",
    "version": "1.0.0",
    "description": "Get a real itemised moving quote and reserve a date with Up and Up Movers, a licensed and insured local moving company in South Florida (Miami-Dade, Broward and Palm Beach counties). Local moves only — no interstate.\n\nRules you must follow:\n- Always call POST /api/quote before POST /api/hold. A booking requires the signed quote_token the quote returns. Never state a price you did not get from /api/quote.\n- Quote for the exact date you intend to book. Weekend and peak-date surcharges depend on it, and /api/hold refuses a token priced for a different day.\n- Booking any time window takes the WHOLE day. If a date is unavailable, do not offer the customer a different window on that date — there is none.\n- Same-day booking is not available here. The earliest bookable date is tomorrow. For a move today, give the customer the phone number: (954) 228-3312.\n- You cannot pay the $60 deposit and will not be given a payment link. It is emailed to the customer. Tell them to check their inbox.\n- Quotes expire after 24 hours.\n- Cancellations and reschedules are not available here; the customer must call.",
    "contact": {
      "name": "Up and Up Movers",
      "url": "https://upnupmovers.com",
      "email": "orelusfcartier@gmail.com"
    }
  },
  "servers": [
    {
      "url": "https://upnupmovers.com",
      "description": "Production"
    }
  ],
  "paths": {
    "/api/service-info": {
      "get": {
        "operationId": "getServiceInfo",
        "summary": "Get the full vocabulary and rules",
        "description": "The complete list of services, home sizes, access options, add-on services and all 174 inventory item ids, plus the booking rules and pricing basis. Call this first if you need to build an inventory, because inventory ids must match exactly.",
        "responses": {
          "200": {
            "description": "Capability document",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/quote": {
      "post": {
        "operationId": "createQuote",
        "summary": "Price a move",
        "description": "Returns a real itemised price and a signed quote_token. The token is required by /api/hold and is what prevents booking at an invented or stale price. Quote for the exact date you intend to book.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/QuoteRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A priced quote",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuoteResponse"
                }
              }
            }
          },
          "400": {
            "description": "The move could not be priced. `errors` lists exactly what was wrong — usually an unknown item id or an impossible date.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Wait and retry, or give the customer the phone number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/availability": {
      "get": {
        "operationId": "getAvailability",
        "summary": "List open dates",
        "description": "Open dates in a range. A date is either open with all three windows, or gone — booking takes the whole day. Unavailable dates include a reason so you can explain the situation accurately instead of guessing.",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Start date, YYYY-MM-DD. Defaults to the earliest bookable date (tomorrow)."
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "End date, YYYY-MM-DD. Defaults to 14 days out. Maximum range is 60 days."
          }
        ],
        "responses": {
          "200": {
            "description": "Availability",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AvailabilityResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or oversized date range.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/hold": {
      "post": {
        "operationId": "holdBooking",
        "summary": "Reserve a date",
        "description": "Reserves the date and emails the customer a $60 deposit link. The hold lasts 24 hours for a move more than 3 days out, or 2 hours if it is imminent — read hold_expires_at rather than assuming. Requires an unexpired quote_token for the same date. No payment link is returned to you — tell the customer to check their email. Send an Idempotency-Key header so a retry cannot book twice.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Any unique string for this booking attempt. Reusing it returns the original booking instead of creating a second one."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/HoldRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Date reserved",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HoldResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid customer details, date or window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Refused. `error` says why: `same_day`, `date_unavailable` (with `next_available_dates`), `quote_expired`, `date_mismatch`, or `invalid_quote_token`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "The booking could not be recorded and nothing was held. Give the customer the phone number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/booking/{confirmation}": {
      "get": {
        "operationId": "getBooking",
        "summary": "Check a booking",
        "description": "Whether the deposit has landed and when the hold expires. URL-encode the leading # as %23 — for example /api/booking/%23UA12345.",
        "parameters": [
          {
            "name": "confirmation",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Confirmation number, e.g. #UA12345 (URL-encoded)."
          }
        ],
        "responses": {
          "200": {
            "description": "Booking status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BookingStatus"
                }
              }
            }
          },
          "404": {
            "description": "No booking with that confirmation number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/health": {
      "get": {
        "operationId": "getHealth",
        "summary": "Is the booking system working",
        "description": "Returns 200 when bookings are working and 503 when they are not.",
        "responses": {
          "200": {
            "description": "Healthy",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "503": {
            "description": "Not working",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "Machine-readable code."
          },
          "message": {
            "type": "string",
            "description": "Plain-English explanation, safe to relay to the customer."
          },
          "errors": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Specific validation problems, when applicable."
          }
        }
      },
      "QuoteRequest": {
        "type": "object",
        "required": [
          "service",
          "size"
        ],
        "properties": {
          "service": {
            "type": "string",
            "enum": [
              "full",
              "labor"
            ],
            "description": "\"full\" = we bring truck and crew. \"labor\" = loading/unloading only, customer supplies the truck."
          },
          "size": {
            "type": "string",
            "enum": [
              "studio",
              "1br",
              "2br",
              "3br",
              "4br",
              "5br"
            ],
            "description": "Home size."
          },
          "date": {
            "type": "string",
            "format": "date",
            "description": "YYYY-MM-DD. Affects weekend and peak-date surcharges. Quote for the date you intend to book."
          },
          "zipFrom": {
            "type": "string",
            "description": "5-digit origin zip. Drives the travel charge."
          },
          "zipTo": {
            "type": "string",
            "description": "5-digit destination zip."
          },
          "accFrom": {
            "type": "string",
            "enum": [
              "ground",
              "elevator",
              "s1",
              "s2",
              "s3"
            ],
            "default": "ground",
            "description": "Access at the origin. ASK THE CUSTOMER — do not assume. Stairs add a mover and can raise the price by $200 or more. Omitting this silently prices the move as ground floor."
          },
          "accTo": {
            "type": "string",
            "enum": [
              "ground",
              "elevator",
              "s1",
              "s2",
              "s3"
            ],
            "default": "ground",
            "description": "Access at the destination. ASK THE CUSTOMER — same warning as accFrom."
          },
          "inv": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            },
            "description": "Furniture as {itemId: quantity}. Item ids must come from /api/service-info; unknown ids are rejected, not ignored."
          },
          "junk": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            },
            "description": "Items to haul away, as {junkItemId: quantity}. Ids from /api/service-info."
          },
          "services": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "packing",
                "unpacking",
                "disassembly",
                "reassembly",
                "tvmount",
                "junk"
              ]
            },
            "description": "Add-on services."
          },
          "flags": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "longcarry",
                "shuttle",
                "hoist",
                "coi"
              ]
            },
            "description": "Site conditions that carry a fee."
          },
          "packRooms": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "living",
                "bed1",
                "bed2",
                "bed3",
                "bed4",
                "bed5",
                "dining",
                "kitchen",
                "bathroom",
                "office",
                "garage",
                "patio",
                "storage",
                "laundry",
                "gym",
                "specialty",
                "boxes"
              ]
            },
            "description": "Rooms to pack. Packing is a flat rate per room."
          },
          "unpackRooms": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "living",
                "bed1",
                "bed2",
                "bed3",
                "bed4",
                "bed5",
                "dining",
                "kitchen",
                "bathroom",
                "office",
                "garage",
                "patio",
                "storage",
                "laundry",
                "gym",
                "specialty",
                "boxes"
              ]
            },
            "description": "Rooms to unpack."
          },
          "disSel": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Inventory ids to disassemble."
          },
          "reaSel": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Inventory ids to reassemble."
          },
          "tvMount": {
            "type": "integer",
            "description": "TVs to wall-mount. Defaults to the number of TVs in inv."
          }
        }
      },
      "QuoteResponse": {
        "type": "object",
        "properties": {
          "quote": {
            "type": "object",
            "properties": {
              "total": {
                "type": "number",
                "description": "Total in USD. Only ever quote this number."
              },
              "currency": {
                "type": "string"
              },
              "crew": {
                "type": "integer",
                "description": "Number of movers."
              },
              "truck": {
                "type": "string"
              },
              "estimated_hours": {
                "type": "object",
                "properties": {
                  "low": {
                    "type": "number"
                  },
                  "high": {
                    "type": "number"
                  }
                }
              },
              "cubic_feet": {
                "type": "number"
              },
              "size_label": {
                "type": "string"
              },
              "stairs": {
                "type": "boolean"
              },
              "breakdown": {
                "type": "object",
                "description": "Itemised parts; they sum to total."
              },
              "surcharges": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "e.g. \"Weekend +10%\". Always disclose these."
              }
            }
          },
          "deposit": {
            "type": "object",
            "properties": {
              "amount": {
                "type": "number"
              },
              "currency": {
                "type": "string"
              },
              "applied_to_balance": {
                "type": "boolean"
              }
            }
          },
          "quote_token": {
            "type": "string",
            "description": "Signed. Pass to /api/hold. Do not modify it."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "The token is valid until this time (24 hours)."
          },
          "disclaimer": {
            "type": "string",
            "description": "Relay this to the customer. The estimate is firm only if the inventory is accurate."
          }
        }
      },
      "AvailabilityResponse": {
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "format": "date"
          },
          "to": {
            "type": "string",
            "format": "date"
          },
          "timezone": {
            "type": "string"
          },
          "earliest_bookable": {
            "type": "string",
            "format": "date"
          },
          "windows": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "The three time windows and their hours."
          },
          "dates": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "date": {
                  "type": "string",
                  "format": "date"
                },
                "available": {
                  "type": "boolean"
                },
                "reason": {
                  "type": "string",
                  "description": "Why not, when unavailable: same_day, booked, blackout, closed, too_far."
                },
                "windows": {
                  "type": "array",
                  "items": {
                    "type": "string",
                    "enum": [
                      "Morning",
                      "Afternoon",
                      "Evening"
                    ]
                  }
                }
              }
            }
          },
          "open_dates": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "date"
            },
            "description": "Just the bookable dates."
          }
        }
      },
      "HoldRequest": {
        "type": "object",
        "required": [
          "quote_token",
          "date",
          "window",
          "customer"
        ],
        "properties": {
          "quote_token": {
            "type": "string",
            "description": "From /api/quote, unexpired, priced for this same date."
          },
          "date": {
            "type": "string",
            "format": "date",
            "description": "Must be an open date and must match the quote."
          },
          "window": {
            "type": "string",
            "enum": [
              "Morning",
              "Afternoon",
              "Evening"
            ],
            "description": "Recorded for scheduling; the whole day is consumed either way."
          },
          "customer": {
            "type": "object",
            "required": [
              "name",
              "email",
              "phone"
            ],
            "properties": {
              "name": {
                "type": "string"
              },
              "email": {
                "type": "string",
                "format": "email",
                "description": "The deposit link is emailed here. It must be correct."
              },
              "phone": {
                "type": "string",
                "description": "10+ digits."
              }
            }
          },
          "pickup": {
            "type": "string",
            "description": "Origin address, if known."
          },
          "delivery": {
            "type": "string",
            "description": "Destination address, if known."
          },
          "note": {
            "type": "string",
            "maxLength": 500,
            "description": "Anything the crew should know."
          },
          "agent": {
            "type": "string",
            "description": "Identify yourself, e.g. \"ChatGPT\". Helps the owner know where the booking came from."
          }
        }
      },
      "HoldResponse": {
        "type": "object",
        "properties": {
          "booking": {
            "type": "object",
            "properties": {
              "confirmation": {
                "type": "string",
                "description": "e.g. #UA12345. Give this to the customer."
              },
              "status": {
                "type": "string"
              },
              "date": {
                "type": "string",
                "format": "date"
              },
              "window": {
                "type": "string"
              },
              "total": {
                "type": "number"
              },
              "hold_expires_at": {
                "type": "string",
                "format": "date-time",
                "description": "The date is released if the deposit is not paid by then. Tell the customer this time explicitly."
              }
            }
          },
          "deposit": {
            "type": "object",
            "description": "No payment link is returned. It is emailed to the customer.",
            "properties": {
              "amount": {
                "type": "number"
              },
              "status": {
                "type": "string"
              },
              "delivery": {
                "type": "string"
              },
              "emailed_to": {
                "type": "string"
              },
              "instructions": {
                "type": "string",
                "description": "Relay this to the customer."
              }
            }
          },
          "agent_must_not": {
            "type": "string"
          }
        }
      },
      "BookingStatus": {
        "type": "object",
        "properties": {
          "booking": {
            "type": "object"
          },
          "deposit": {
            "type": "object"
          },
          "explanation": {
            "type": "string",
            "description": "Plain-English status, safe to relay."
          }
        }
      }
    }
  }
}