API reference
Turn a described shoot into a structured, priced request — and route it to real local videographers. Deterministic (no LLM), calibrated to real production rates. No key, no auth, free.
Quick start
Post a plain-language shoot description and get a structured request plus a calibrated budget estimate:
curl -s https://videographer.com/api/quote \
-H 'content-type: application/json' \
-d '{"description":"a 5-hour wedding in Austin, two cameras, drone"}'
Authentication
The pricing endpoints need no authentication. Creating a request with POST /api/order returns an owner token — a secret capability you pass back to broadcast or manage it. Write endpoints are rate-limited per IP.
Videographers
/api/videographersAdd a videographer to the router
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.
| Field | Type | Req | Description |
|---|---|---|---|
name | string | — | Name or studio, as it should appear publicly. |
email | string | yes | 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 | string | — | Comma-separated cities covered, e.g. "Austin, Dallas". |
shootTypes | array | — | What they shoot. Requests are routed on city and shoot type. |
curl -s https://videographer.com/api/videographers \
-H 'content-type: application/json' \
-d '{}'200 Listed. Returns the new id.
Supply
/api/join/detectRead a videographer's own site into a draft listing
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.
| Field | Type | Req | Description |
|---|---|---|---|
site | string | yes | A domain or URL. A bare domain is treated as https. |
curl -s https://videographer.com/api/join/detect \
-H 'content-type: application/json' \
-d '{"site":"harbourfilms.co"}'200 A draft, or detected:false with a reason. Never an error for an unreadable site.
/api/joinCreate a free listing from an email
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.
| Field | Type | Req | Description |
|---|---|---|---|
email | string | yes | |
name | string | — | |
bio | string | — | |
url | string | — | |
cities | string | — | Comma-separated, or an array. |
shootTypes | string | — | Comma-separated, or an array. |
curl -s https://videographer.com/api/join \
-H 'content-type: application/json' \
-d '{"email":"[email protected]","name":"Harbour Films","bio":"Wedding films across Chicago.","cities":"Chicago, Milwaukee","shootTypes":["wedding"]}'200 Listed. A sign-in link is queued to the address given; delivery depends on the deployment.
Routing
/api/leadSend a described shoot to matching videographers
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.
| Field | Type | Req | Description |
|---|---|---|---|
description | string | yes | The shoot in plain language. Priced server-side; this is the only input the numbers come from. |
email | string | yes | Where the videographer replies. |
name | string | — | |
phone | string | — | |
website | string | — | HONEYPOT — leave unset. A value here is accepted and discarded. |
curl -s https://videographer.com/api/lead \
-H 'content-type: application/json' \
-d '{"description":"a 6-hour wedding in Chicago, two cameras","email":"[email protected]","name":"Sam Rivera"}'200 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. LeadResponse
Pricing
Describe a shoot → a structured request with a calibrated budget estimate.
/api/quotePrice a described shoot
Natural-language shoot description in, a structured request + calibrated budget estimate out. This is the engine behind the homepage builder.
| Field | Type | Req | Description |
|---|---|---|---|
description | string | yes | Plain-language shoot description. |
curl -s https://videographer.com/api/quote \
-H 'content-type: application/json' \
-d '{"description":"a 5-hour wedding in Austin, two cameras, drone"}'200 A structured request and a budget estimate. QuoteResponse
{
"order": {
"shootType": "wedding",
"city": "Austin",
"locality": null,
"cityTier": 3,
"cityFactor": 1.05,
"date": null,
"dateISO": null,
"datePhrase": null,
"durationMinutes": 300,
"deliverableMinutes": null,
"cameras": 2,
"addons": [
"drone"
],
"budget": null,
"budgetAmount": null,
"startMinutes": null
},
"estimate": {
"currency": "USD",
"range": {
"low": 1675,
"high": 3650
},
"display": "$1,675 – $3,650",
"tiers": [
{
"key": "essentials",
"label": "Essentials",
"display": "$1,675 – $2,275",
"includes": [
"5h coverage",
"2 cameras",
"…"
]
},
{
"key": "signature",
"label": "Signature",
"display": "$2,275 – $3,250",
"includes": [
"5h coverage",
"2 cameras",
"…"
]
},
{
"key": "cinematic",
"label": "Cinematic",
"display": "$3,250 – $3,650",
"includes": [
"5h coverage",
"2 cameras",
"…"
]
}
],
"recommended": {
"cameras": 2,
"crew": 2,
"coverageHours": 5,
"deliverables": [
"Raw footage (all coverage)",
"Documentary edit option",
"Highlight film (3–6 min)",
"Color-graded delivery",
"Aerial / drone"
],
"turnaround": "4–8 weeks"
},
"lineItems": [
{
"label": "Wedding coverage + edit (raw → finished)",
"low": 1150,
"high": 2725
},
{
"label": "Additional camera (×1)",
"low": 200,
"high": 200
},
{
"label": "Aerial / drone",
"low": 300,
"high": 700
}
],
"assumptions": [
"5h coverage",
"2 cameras",
"…"
],
"note": "Estimate only — a real quote depends on the specific shoot, scope, and videographer. Ranges reflect real production pricing."
}
}/api/estimatePrice an already-structured request
If you already have a structured request (shoot type, city tier, duration, cameras, crew, add-ons), get just the budget estimate. Deterministic.
| Field | Type | Req | Description |
|---|---|---|---|
shootType | string · wedding | corporate-event | brand-film | real-estate | music-video | other | — | |
cityTier | integer · 1 | 2 | 3 | — | 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 | integer | — | |
cameras | integer | — | |
crew | integer | — | |
addons | array | — | e.g. drone, second-shooter, same-day-edit. |
curl -s https://videographer.com/api/estimate \
-H 'content-type: application/json' \
-d '{"shootType":"wedding","cityTier":1,"durationMinutes":300,"cameras":2,"crew":1,"addons":["drone"]}'200 The budget estimate.
{
"estimate": {
"currency": "USD",
"range": {
"low": 1675,
"high": 3650
},
"display": "$1,675 – $3,650",
"tiers": [
{
"key": "essentials",
"label": "Essentials",
"display": "$1,675 – $2,275",
"includes": [
"5h coverage",
"2 cameras",
"…"
]
},
{
"key": "signature",
"label": "Signature",
"display": "$2,275 – $3,250",
"includes": [
"5h coverage",
"2 cameras",
"…"
]
},
{
"key": "cinematic",
"label": "Cinematic",
"display": "$3,250 – $3,650",
"includes": [
"5h coverage",
"2 cameras",
"…"
]
}
],
"recommended": {
"cameras": 2,
"crew": 2,
"coverageHours": 5,
"deliverables": [
"Raw footage (all coverage)",
"Documentary edit option",
"Highlight film (3–6 min)",
"Color-graded delivery",
"Aerial / drone"
],
"turnaround": "4–8 weeks"
},
"lineItems": [
{
"label": "Wedding coverage + edit (raw → finished)",
"low": 1150,
"high": 2725
},
{
"label": "Additional camera (×1)",
"low": 200,
"high": 200
},
{
"label": "Aerial / drone",
"low": 300,
"high": 700
}
],
"assumptions": [
"5h coverage",
"2 cameras",
"…"
],
"note": "Estimate only — a real quote depends on the specific shoot, scope, and videographer. Ranges reflect real production pricing."
}
}Orders
Create a real, bookmarkable order and route it to matching videographers.
/api/orderCreate a request
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.
| Field | Type | Req | Description |
|---|---|---|---|
description | string | yes | Plain-language shoot description. |
curl -s https://videographer.com/api/order \
-H 'content-type: application/json' \
-d '{"description":"brand film, half day, one location, Chicago"}'200 The created order id + owner token.
{
"id": "5a47a8f3-d3d4-4af8-99b1-f07c2abcb0ea",
"token": "9c2f0b1e-4a77-4e2b-b0c1-7f3d2a1e6b84"
}/api/order/{id}Get order status
The live status of a request derived from its routes — open, routed, or accepted (with the accepting videographer once accepted).
| Field | Type | Req | Description |
|---|---|---|---|
id | string · path | yes |
curl -s https://videographer.com/api/order/{id}200 The order status view. OrderStatus
{
"id": "5a47a8f3-d3d4-4af8-99b1-f07c2abcb0ea",
"created_at": "2026-07-24T18:02:11.904Z",
"status": "open",
"matchCount": 3,
"routedCount": 0,
"shootType": "wedding",
"city": "Austin",
"order": {
"shootType": "wedding",
"city": "Austin",
"locality": null,
"cityTier": 3,
"cityFactor": 1.05,
"date": null,
"dateISO": null,
"datePhrase": null,
"durationMinutes": 300,
"deliverableMinutes": null,
"cameras": 2,
"addons": [
"drone"
],
"budget": null,
"budgetAmount": null,
"startMinutes": null
},
"estimate": {
"currency": "USD",
"range": {
"low": 1675,
"high": 3650
},
"display": "$1,675 – $3,650",
"tiers": [
{
"key": "essentials",
"label": "Essentials",
"display": "$1,675 – $2,275",
"includes": [
"5h coverage",
"2 cameras",
"…"
]
},
{
"key": "signature",
"label": "Signature",
"display": "$2,275 – $3,250",
"includes": [
"5h coverage",
"2 cameras",
"…"
]
},
{
"key": "cinematic",
"label": "Cinematic",
"display": "$3,250 – $3,650",
"includes": [
"5h coverage",
"2 cameras",
"…"
]
}
],
"recommended": {
"cameras": 2,
"crew": 2,
"coverageHours": 5,
"deliverables": [
"Raw footage (all coverage)",
"Documentary edit option",
"Highlight film (3–6 min)",
"Color-graded delivery",
"Aerial / drone"
],
"turnaround": "4–8 weeks"
},
"lineItems": [
{
"label": "Wedding coverage + edit (raw → finished)",
"low": 1150,
"high": 2725
},
{
"label": "Additional camera (×1)",
"low": 200,
"high": 200
},
{
"label": "Aerial / drone",
"low": 300,
"high": 700
}
],
"assumptions": [
"5h coverage",
"2 cameras",
"…"
],
"note": "Estimate only — a real quote depends on the specific shoot, scope, and videographer. Ranges reflect real production pricing."
},
"description": "a 5-hour wedding in Austin, two cameras, drone",
"videographer": null,
"isOwner": false
}/api/order/{id}/broadcastBroadcast a request to matching videographers
Route the order to matching local videographers (pull-model inbox). Requires the owner token. Idempotent — re-broadcasting a routed order is a no-op.
| Field | Type | Req | Description |
|---|---|---|---|
id | string · path | yes | |
token | string | yes | The owner token from create. |
curl -s https://videographer.com/api/order/{id}/broadcast \
-H 'content-type: application/json' \
-d '{}'200 Routed count.
{
"ok": true,
"routed": 2
}Crew
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.
/api/account/second-shooterOpt yourself in or out of the second-shooter network
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.
| Field | Type | Req | Description |
|---|---|---|---|
discoverable | boolean | — | Absent or false takes you OUT of the network. Nothing else in the crew API returns you until this is true. |
note | string | — | Optional. Truncated, not rejected, past the limit. |
roles | array | — |
curl -s https://videographer.com/api/account/second-shooter \
-H 'content-type: application/json' \
-d '{"discoverable":true,"note":"Happy to shoot second on weddings.","roles":["second-shooter"]}'200 The opt-in as stored, echoed back.
/api/account/availabilityMark your own days available or busy
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.
| Field | Type | Req | Description |
|---|---|---|---|
date | string | — | YYYY-MM-DD. Anything else is silently skipped. |
status | string | — | `available` or `busy` set the day. Any other value CLEARS it — that is how you unmark a day. |
updates | array | — |
curl -s https://videographer.com/api/account/availability \
-H 'content-type: application/json' \
-d '{"updates":[{"date":"2026-08-15","status":"busy"}]}'200 The whole map as stored, echoed back so a calendar can re-render from the server copy rather than its own guess.
/api/second-shooterSearch the network for an available peer
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.
| Field | Type | Req | Description |
|---|---|---|---|
date | string | yes | YYYY-MM-DD. |
city | string | yes | Defaults to your own first coverage city. |
shootType | string | yes |
curl -s https://videographer.com/api/second-shooter
200 Matching peers. `count: 0` is a real answer, not an error.
/api/second-shooter/requestAsk a peer to shoot a date with you
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.
| Field | Type | Req | Description |
|---|---|---|---|
toId | string | yes | The videographer being asked. Must be discoverable and not yourself. |
date | string | yes | YYYY-MM-DD. Required and validated. |
shootType | string | — | |
note | string | — | Truncated, not rejected, past the limit. |
curl -s https://videographer.com/api/second-shooter/request \
-H 'content-type: application/json' \
-d '{"toId":"…","date":"2026-08-15","shootType":"wedding","note":"Ceremony at 3, second angle on the aisle."}'200 Created, or the same pending ask returned unchanged.
/api/second-shooter/request/{id}/{action}Accept or decline an inbound request
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.
| Field | Type | Req | Description |
|---|---|---|---|
id | string · path | yes | |
action | string · path | yes |
curl -s https://videographer.com/api/second-shooter/request/{id}/{action} \
-H 'content-type: application/json' \
-d '{}'200 The new status, plus the requester's contact on accept.
/api/account/connectionsYour crew requests, both directions
Inbound pending (the actionable ones), outbound pending, and accepted in both directions. Email appears only on accepted rows. Session-authenticated.
curl -s https://videographer.com/api/account/connections
200 The connections view.
Account
/api/account/listingHide or restore this listing without closing the account
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.
| Field | Type | Req | Description |
|---|---|---|---|
listed | boolean | — | True republishes; false or absent hides. |
curl -s https://videographer.com/api/account/listing \
-H 'content-type: application/json' \
-d '{"listed":false}'200 The status as stored.
MCP server
JSON-RPC 2.0 over stdio. The server advertises 12 tools: three price a shoot in-process with no account at all, and the rest act for one signed-in videographer — their calendar, their leads, their listing — and need a credential that names that account.
{
"mcpServers": {
"videographer": {
"command": "node",
"args": ["/absolute/path/to/videographer.com/apps/com/app/mcp.js"]
}
}
}
The tool table, one full descriptor, a captured tools/call, and both refusals are on the agents page — replayed from a real session rather than described.
Answer engines
Machine-readable endpoints and real cost data are published at /llms.txt for citation by LLMs, and the full contract is at /openapi.json.
Errors & rate limits
Errors return a JSON body { "error": "…" } with a 4xx status. Write endpoints (/api/quote, /api/order, broadcast) are throttled per client IP; exceeding the window returns 429. The per-minute number for every one of them is published on Security.
One refusal is not an error and does not clear on a retry: a shoot we do not cover comes back 200 carrying a refusal instead of a price, and no other endpoint will price it either. Bereavements, milestones and one-off ceremonies are the list. Do not reword the description to get around it.