{
  "openapi": "3.1.0",
  "info": {
    "title": "Videographer.com API",
    "version": "1.0.0",
    "description": "Turn a described video shoot into a calibrated, structured, priced request — and create a real, routable request object. Deterministic (no LLM), calibrated to real production rates. No auth, no key, free.",
    "contact": {
      "name": "Videographer.com",
      "url": "https://videographer.com/agents"
    }
  },
  "servers": [
    {
      "url": "https://videographer.com",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Pricing",
      "description": "Describe a shoot → a structured request with a calibrated budget estimate."
    },
    {
      "name": "Orders",
      "description": "Create a real, bookmarkable order and route it to matching videographers."
    },
    {
      "name": "Crew",
      "description": "Videographers hiring each other: mark your days, find an available peer, ask them to shoot with you. Served by videographer.com AND videographer.org from this one router. Session-authenticated — cookie only, no token form."
    }
  ],
  "paths": {
    "/api/videographers": {
      "post": {
        "tags": [
          "Videographers"
        ],
        "summary": "Add a videographer to the router",
        "operationId": "createVideographer",
        "description": "Free, and the only thing needed to start receiving routed requests. No card, no commission, no fee per lead. An email already listed is refused rather than duplicated.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Name or studio, as it should appear publicly."
                  },
                  "email": {
                    "type": "string",
                    "format": "email",
                    "description": "Identifies the account routed requests are held for, and the address they are mailed to where this deployment has mail armed. Never shown publicly."
                  },
                  "cities": {
                    "type": "string",
                    "description": "Comma-separated cities covered, e.g. \"Austin, Dallas\"."
                  },
                  "shootTypes": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "wedding",
                        "corporate-event",
                        "brand-film",
                        "real-estate",
                        "music-video"
                      ]
                    },
                    "description": "What they shoot. Requests are routed on city and shoot type."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Listed. Returns the new id."
          },
          "400": {
            "description": "A valid email is required."
          },
          "409": {
            "description": "That email is already listed — sign in instead."
          }
        }
      }
    },
    "/api/join/detect": {
      "post": {
        "tags": [
          "Supply"
        ],
        "summary": "Read a videographer's own site into a draft listing",
        "operationId": "joinDetect",
        "description": "A domain in, a PROPOSED name, one-line bio, cities and shoot types out. Nothing is stored — this returns a draft of the join form for a person to correct. The fetch is restricted to public http(s) addresses: loopback, RFC1918, carrier-grade NAT and link-local (including the cloud metadata endpoint) are refused, on every redirect hop, after DNS resolution. A site that cannot be read answers 200 with detected:false rather than an error, because the form still works by hand.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "site"
                ],
                "properties": {
                  "site": {
                    "type": "string",
                    "description": "A domain or URL. A bare domain is treated as https."
                  }
                }
              },
              "example": {
                "site": "harbourfilms.co"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A draft, or detected:false with a reason. Never an error for an unreadable site.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "detected": {
                      "type": "boolean"
                    },
                    "url": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    },
                    "fix": {
                      "type": "string"
                    },
                    "draft": {
                      "type": "object",
                      "properties": {
                        "name": {
                          "type": "string"
                        },
                        "bio": {
                          "type": "string"
                        },
                        "cities": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "shootTypes": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/join": {
      "post": {
        "tags": [
          "Supply"
        ],
        "summary": "Create a free listing from an email",
        "operationId": "join",
        "description": "The self-serve counterpart to operator onboarding. An email is the only required field; everything else is optional and usually arrives from /api/join/detect after the person has corrected it. A bio matters more than it looks: a listing is shown in the public directory only when it carries a bio, a reel or a work piece, so a listing created without one is signed up and invisible. No sign-in link is returned in the response; one is QUEUED to the address given, and whether it leaves depends on whether the deployment has mail armed.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "bio": {
                    "type": "string"
                  },
                  "url": {
                    "type": "string"
                  },
                  "cities": {
                    "type": "string",
                    "description": "Comma-separated, or an array."
                  },
                  "shootTypes": {
                    "type": "string",
                    "description": "Comma-separated, or an array."
                  }
                }
              },
              "example": {
                "email": "hello@harbourfilms.co",
                "name": "Harbour Films",
                "bio": "Wedding films across Chicago.",
                "cities": "Chicago, Milwaukee",
                "shootTypes": [
                  "wedding"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Listed. A sign-in link is queued to the address given; delivery depends on the deployment.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "id": {
                      "type": "string"
                    },
                    "email": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A valid email is required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "That address already has an account; sign in instead.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/lead": {
      "post": {
        "tags": [
          "Routing"
        ],
        "summary": "Send a described shoot to matching videographers",
        "operationId": "lead",
        "description": "Contact details plus a shoot description in; the request is priced server-side, stored, and routed to matching videographers. The order and estimate are ALWAYS re-derived from the description here — a caller's own numbers are never trusted. Capped at three videographers per request, and a shoot we decline to price is stored without one and routed to nobody.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LeadRequest"
              },
              "example": {
                "description": "a 6-hour wedding in Chicago, two cameras",
                "email": "you@example.com",
                "name": "Sam Rivera"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Stored, with how many videographers it reached. `routed: 0` is a real answer, not an error — it means nobody covers that work in that city yet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LeadResponse"
                }
              }
            }
          },
          "400": {
            "description": "A valid email is required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/quote": {
      "post": {
        "tags": [
          "Pricing"
        ],
        "summary": "Price a described shoot",
        "operationId": "quote",
        "description": "Natural-language shoot description in, a structured request + calibrated budget estimate out. This is the engine behind the homepage builder.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/QuoteRequest"
              },
              "example": {
                "description": "a 5-hour wedding in Austin, two cameras, drone"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A structured request and a budget estimate.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuoteResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing description.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/estimate": {
      "post": {
        "tags": [
          "Pricing"
        ],
        "summary": "Price an already-structured request",
        "operationId": "estimate",
        "description": "If you already have a structured request (shoot type, city tier, duration, cameras, crew, add-ons), get just the budget estimate. Deterministic.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EstimateRequest"
              },
              "example": {
                "shootType": "wedding",
                "cityTier": 1,
                "durationMinutes": 300,
                "cameras": 2,
                "crew": 1,
                "addons": [
                  "drone"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The budget estimate.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "estimate": {
                      "$ref": "#/components/schemas/Estimate"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/order": {
      "post": {
        "tags": [
          "Orders"
        ],
        "summary": "Create a request",
        "operationId": "createOrder",
        "description": "Create a real, bookmarkable order object from a described shoot. Returns an id and an owner token — keep the token to broadcast + manage the order.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/QuoteRequest"
              },
              "example": {
                "description": "brand film, half day, one location, Chicago"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The created order id + owner token.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "token": {
                      "type": "string",
                      "format": "uuid",
                      "description": "Owner capability — required to broadcast/manage. Keep it secret."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing description or unrecognized shoot type.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/order/{id}": {
      "get": {
        "tags": [
          "Orders"
        ],
        "summary": "Get order status",
        "operationId": "getOrder",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "description": "The live status of a request derived from its routes — open, routed, or accepted (with the accepting videographer once accepted).",
        "responses": {
          "200": {
            "description": "The order status view.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrderStatus"
                }
              }
            }
          },
          "404": {
            "description": "No such order.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/order/{id}/broadcast": {
      "post": {
        "tags": [
          "Orders"
        ],
        "summary": "Broadcast a request to matching videographers",
        "operationId": "broadcastOrder",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "description": "Route the order to matching local videographers (pull-model inbox). Requires the owner token. Idempotent — re-broadcasting a routed order is a no-op.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "token"
                ],
                "properties": {
                  "token": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The owner token from create."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Routed count.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "routed": {
                      "type": "integer"
                    },
                    "alreadyBroadcast": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Wrong or missing owner token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/account/second-shooter": {
      "post": {
        "tags": [
          "Crew"
        ],
        "summary": "Opt yourself in or out of the second-shooter network",
        "operationId": "setSecondShooter",
        "description": "Set whether you are discoverable to other videographers, with an optional note and roles. THIS IS THE GATE: peer search returns only discoverable videographers, so nothing else in the crew API does anything until this is on. Absent discoverable means OFF — an opt-in never defaults to joining. Writes ONLY these three fields; name, url, bio, cities and works are not reachable here.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SecondShooterOptIn"
              },
              "example": {
                "discoverable": true,
                "note": "Happy to shoot second on weddings.",
                "roles": [
                  "second-shooter"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The opt-in as stored, echoed back.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "secondShooter": {
                      "type": [
                        "object",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/account/availability": {
      "post": {
        "tags": [
          "Crew"
        ],
        "summary": "Mark your own days available or busy",
        "operationId": "setAvailability",
        "description": "Patch the availability map on your videographer record. Accepts a single {date,status} or bulk {updates:[…]}. Status `available` or `busy` sets the day; ANY OTHER VALUE CLEARS IT — the map is sparse and stores only explicit marks, so clearing is how you unmark rather than an error. Session-authenticated.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AvailabilityPatch"
              },
              "example": {
                "updates": [
                  {
                    "date": "2026-08-15",
                    "status": "busy"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The whole map as stored, echoed back so a calendar can re-render from the server copy rather than its own guess.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "availability": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "string",
                        "enum": [
                          "available",
                          "busy"
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/second-shooter": {
      "get": {
        "tags": [
          "Crew"
        ],
        "summary": "Search the network for an available peer",
        "operationId": "searchSecondShooters",
        "description": "Videographers who are available on the date, opted in as discoverable, and match. City defaults to your own first coverage city. PUBLIC FIELDS ONLY — email is never in this response; contact is revealed by acceptance, not by search. Session-authenticated.",
        "parameters": [
          {
            "name": "date",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "YYYY-MM-DD."
          },
          {
            "name": "city",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Defaults to your own first coverage city."
          },
          {
            "name": "shootType",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Matching peers. `count: 0` is a real answer, not an error.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "count": {
                      "type": "integer"
                    },
                    "date": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "city": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "shootType": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "results": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/second-shooter/request": {
      "post": {
        "tags": [
          "Crew"
        ],
        "summary": "Ask a peer to shoot a date with you",
        "operationId": "requestSecondShooter",
        "description": "Creates a pending connection and queues a heads-up email. WHETHER IT IS ACTUALLY DELIVERED DEPENDS ON THIS DEPLOYMENT: mail sends only when the operator has armed it, and the row is written with status `queued` and `sent_at: null` either way. Do not build on the mail arriving — the person you asked sees the request when they sign in, which is true in both configurations. ONE ASK PER (asker, asked, date), EVER. A repeat of a still-pending ask is idempotent and returns `already: true`. A repeat after an ANSWER is 409 — a decline is an answer, and re-asking the same person the same date is nagging, which is the fastest way to lose the supply side. Session-authenticated.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SecondShooterRequest"
              },
              "example": {
                "toId": "…",
                "date": "2026-08-15",
                "shootType": "wedding",
                "note": "Ceremony at 3, second angle on the aisle."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Created, or the same pending ask returned unchanged.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "id": {
                      "type": "string"
                    },
                    "already": {
                      "type": "boolean",
                      "description": "True when this exact ask was already pending."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "No such videographer, yourself, or a date that is not YYYY-MM-DD.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Not open to requests, or this date was already answered. Carries `status`. NOT a retryable error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/second-shooter/request/{id}/{action}": {
      "post": {
        "tags": [
          "Crew"
        ],
        "summary": "Accept or decline an inbound request",
        "operationId": "answerSecondShooterRequest",
        "description": "Only the RECIPIENT may act, and only on a pending request. Accept is a mutual contact reveal: the accepter receives the requester's contact in this response, and the requester learns the accepter's email from /api/account/connections. That asymmetry is deliberate — nobody's address is disclosed before they agree. A notification to the requester is queued, and delivered only if this deployment has mail armed — so /api/account/connections is the reliable way they find out, in both configurations. Any dashboard link inside a queued crew notification points at videographer.com even when the request was made through videographer.org, because those links are minted from the canonical origin rather than the request Host — a link built from a spoofed Host is how emailed links get poisoned.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "action",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "accept",
                "decline"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The new status, plus the requester's contact on accept.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "accepted",
                        "declined"
                      ]
                    },
                    "contact": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "properties": {
                        "name": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "email": {
                          "type": [
                            "string",
                            "null"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No such request, or it was not addressed to you. Same answer either way, on purpose.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Already answered.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/account/connections": {
      "get": {
        "tags": [
          "Crew"
        ],
        "summary": "Your crew requests, both directions",
        "operationId": "listConnections",
        "description": "Inbound pending (the actionable ones), outbound pending, and accepted in both directions. Email appears only on accepted rows. Session-authenticated.",
        "responses": {
          "200": {
            "description": "The connections view.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/account/listing": {
      "post": {
        "tags": [
          "Account"
        ],
        "summary": "Hide or restore this listing without closing the account",
        "operationId": "setListingVisibility",
        "description": "Takes the listing down everywhere, or puts it back up. Absent `listed` means HIDE — a body-less post must not silently republish someone. Unlisted removes the videographer.com profile and the videographer.net directory row, because both require status \"active\"; sign-in and agent credentials keep working, since those gate on \"deleted\". This is not deletion and nothing is scrubbed.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "listed": {
                    "type": "boolean",
                    "description": "True republishes; false or absent hides."
                  }
                }
              },
              "example": {
                "listed": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The status as stored.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "active",
                        "unlisted"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "QuoteRequest": {
        "type": "object",
        "required": [
          "description"
        ],
        "properties": {
          "description": {
            "type": "string",
            "maxLength": 2000,
            "description": "Plain-language shoot description."
          }
        }
      },
      "SecondShooterOptIn": {
        "type": "object",
        "properties": {
          "discoverable": {
            "type": "boolean",
            "description": "Absent or false takes you OUT of the network. Nothing else in the crew API returns you until this is true."
          },
          "note": {
            "type": "string",
            "maxLength": 280,
            "description": "Optional. Truncated, not rejected, past the limit."
          },
          "roles": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 40
            },
            "maxItems": 8
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          }
        }
      },
      "AvailabilityPatch": {
        "type": "object",
        "description": "Either a single day or a batch. If `updates` is present and an array, the single-day fields are ignored entirely.",
        "properties": {
          "date": {
            "type": "string",
            "format": "date",
            "description": "YYYY-MM-DD. Anything else is silently skipped."
          },
          "status": {
            "type": "string",
            "description": "`available` or `busy` set the day. Any other value CLEARS it — that is how you unmark a day."
          },
          "updates": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "date": {
                  "type": "string",
                  "format": "date"
                },
                "status": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "SecondShooterRequest": {
        "type": "object",
        "required": [
          "toId",
          "date"
        ],
        "properties": {
          "toId": {
            "type": "string",
            "description": "The videographer being asked. Must be discoverable and not yourself."
          },
          "date": {
            "type": "string",
            "format": "date",
            "description": "YYYY-MM-DD. Required and validated."
          },
          "shootType": {
            "type": "string",
            "maxLength": 40
          },
          "note": {
            "type": "string",
            "maxLength": 280,
            "description": "Truncated, not rejected, past the limit."
          }
        }
      },
      "EstimateRequest": {
        "type": "object",
        "properties": {
          "shootType": {
            "type": "string",
            "enum": [
              "wedding",
              "corporate-event",
              "brand-film",
              "real-estate",
              "music-video",
              "other"
            ]
          },
          "cityTier": {
            "type": "integer",
            "enum": [
              1,
              2,
              3
            ],
            "description": "Cost band: 1 = highest-cost metros (SF/LA), 2 = major metros (Chicago/Boston/Seattle), 3 = standard markets. A finer per-city cityFactor is applied on top."
          },
          "durationMinutes": {
            "type": "integer"
          },
          "cameras": {
            "type": "integer"
          },
          "crew": {
            "type": "integer"
          },
          "addons": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "e.g. drone, second-shooter, same-day-edit."
          }
        }
      },
      "Order": {
        "type": "object",
        "description": "A structured shoot order, parsed from the description.",
        "properties": {
          "shootType": {
            "type": "string",
            "enum": [
              "wedding",
              "corporate-event",
              "brand-film",
              "real-estate",
              "music-video",
              "other"
            ]
          },
          "city": {
            "type": "string",
            "nullable": true
          },
          "cityTier": {
            "type": "integer"
          },
          "cityFactor": {
            "type": "number",
            "description": "City cost multiplier applied to the estimate."
          },
          "date": {
            "type": "string",
            "nullable": true
          },
          "durationMinutes": {
            "type": "integer"
          },
          "cameras": {
            "type": "integer"
          },
          "addons": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "budget": {
            "type": "string",
            "nullable": true
          },
          "budgetAmount": {
            "type": "integer",
            "nullable": true,
            "description": "Client-stated budget, if any."
          }
        }
      },
      "Tier": {
        "type": "object",
        "description": "One of three package tiers spanning the range.",
        "properties": {
          "key": {
            "type": "string",
            "enum": [
              "essentials",
              "signature",
              "cinematic"
            ]
          },
          "label": {
            "type": "string"
          },
          "tagline": {
            "type": "string"
          },
          "priceLow": {
            "type": "integer"
          },
          "priceHigh": {
            "type": "integer"
          },
          "display": {
            "type": "string"
          },
          "includes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "Estimate": {
        "type": "object",
        "description": "A calibrated budget estimate — a planning range, not a quote. Includes three package tiers and the line items + assumptions behind the number.",
        "properties": {
          "currency": {
            "type": "string",
            "example": "USD"
          },
          "range": {
            "type": "object",
            "properties": {
              "low": {
                "type": "integer"
              },
              "high": {
                "type": "integer"
              }
            }
          },
          "display": {
            "type": "string",
            "example": "$1,675 – $3,650"
          },
          "tiers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Tier"
            }
          },
          "recommended": {
            "type": "object",
            "properties": {
              "cameras": {
                "type": "integer"
              },
              "crew": {
                "type": "integer"
              },
              "coverageHours": {
                "type": "integer"
              },
              "deliverables": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "turnaround": {
                "type": "string"
              }
            }
          },
          "lineItems": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "label": {
                  "type": "string"
                },
                "low": {
                  "type": "integer"
                },
                "high": {
                  "type": "integer"
                }
              }
            }
          },
          "assumptions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "note": {
            "type": "string"
          }
        }
      },
      "QuoteResponse": {
        "type": "object",
        "properties": {
          "order": {
            "$ref": "#/components/schemas/Order"
          },
          "estimate": {
            "$ref": "#/components/schemas/Estimate"
          }
        }
      },
      "OrderStatus": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "status": {
            "type": "string",
            "enum": [
              "open",
              "routed",
              "accepted"
            ]
          },
          "matchCount": {
            "type": "integer",
            "description": "Videographers matching this order."
          },
          "routedCount": {
            "type": "integer",
            "description": "How many it has been broadcast to."
          },
          "shootType": {
            "type": "string"
          },
          "city": {
            "type": "string",
            "nullable": true
          },
          "order": {
            "$ref": "#/components/schemas/Order"
          },
          "estimate": {
            "$ref": "#/components/schemas/Estimate"
          },
          "description": {
            "type": "string"
          },
          "videographer": {
            "type": "object",
            "nullable": true,
            "description": "The accepting videographer once accepted (name + optional site).",
            "properties": {
              "name": {
                "type": "string"
              },
              "url": {
                "type": "string"
              }
            }
          },
          "isOwner": {
            "type": "boolean",
            "description": "True when the request carried the owner token."
          }
        }
      },
      "LeadRequest": {
        "type": "object",
        "required": [
          "description",
          "email"
        ],
        "properties": {
          "description": {
            "type": "string",
            "description": "The shoot in plain language. Priced server-side; this is the only input the numbers come from."
          },
          "email": {
            "type": "string",
            "description": "Where the videographer replies."
          },
          "name": {
            "type": "string"
          },
          "phone": {
            "type": "string"
          },
          "website": {
            "type": "string",
            "description": "HONEYPOT — leave unset. A value here is accepted and discarded."
          }
        }
      },
      "LeadResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "id": {
            "type": "string"
          },
          "routed": {
            "type": "integer",
            "description": "How many videographers received it. Zero means none cover that work in that city."
          }
        }
      }
    }
  }
}