videographer.com · api

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

POST/api/videographers

Add 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.

FieldTypeReqDescription
namestringName or studio, as it should appear publicly.
emailstringyesIdentifies the account routed requests are held for, and the address they are mailed to where this deployment has mail armed. Never shown publicly.
citiesstringComma-separated cities covered, e.g. "Austin, Dallas".
shootTypesarrayWhat they shoot. Requests are routed on city and shoot type.
Request
curl -s https://videographer.com/api/videographers \
  -H 'content-type: application/json' \
  -d '{}'
Returns

200 Listed. Returns the new id.

Supply

POST/api/join/detect

Read 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.

FieldTypeReqDescription
sitestringyesA domain or URL. A bare domain is treated as https.
Request
curl -s https://videographer.com/api/join/detect \
  -H 'content-type: application/json' \
  -d '{"site":"harbourfilms.co"}'
Returns

200 A draft, or detected:false with a reason. Never an error for an unreadable site.

POST/api/join

Create 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.

FieldTypeReqDescription
emailstringyes
namestring
biostring
urlstring
citiesstringComma-separated, or an array.
shootTypesstringComma-separated, or an array.
Request
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"]}'
Returns

200 Listed. A sign-in link is queued to the address given; delivery depends on the deployment.

Routing

POST/api/lead

Send 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.

FieldTypeReqDescription
descriptionstringyesThe shoot in plain language. Priced server-side; this is the only input the numbers come from.
emailstringyesWhere the videographer replies.
namestring
phonestring
websitestringHONEYPOT — leave unset. A value here is accepted and discarded.
Request
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"}'
Returns

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.

POST/api/quote

Price a described shoot

Natural-language shoot description in, a structured request + calibrated budget estimate out. This is the engine behind the homepage builder.

FieldTypeReqDescription
descriptionstringyesPlain-language shoot description.
Request
curl -s https://videographer.com/api/quote \
  -H 'content-type: application/json' \
  -d '{"description":"a 5-hour wedding in Austin, two cameras, drone"}'
Returns

200 A structured request and a budget estimate. QuoteResponse

Example response
{
  "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."
  }
}
POST/api/estimate

Price 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.

FieldTypeReqDescription
shootTypestring · wedding | corporate-event | brand-film | real-estate | music-video | other
cityTierinteger · 1 | 2 | 3Cost 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.
durationMinutesinteger
camerasinteger
crewinteger
addonsarraye.g. drone, second-shooter, same-day-edit.
Request
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"]}'
Returns

200 The budget estimate.

Example response
{
  "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.

POST/api/order

Create 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.

FieldTypeReqDescription
descriptionstringyesPlain-language shoot description.
Request
curl -s https://videographer.com/api/order \
  -H 'content-type: application/json' \
  -d '{"description":"brand film, half day, one location, Chicago"}'
Returns

200 The created order id + owner token.

Example response
{
  "id": "5a47a8f3-d3d4-4af8-99b1-f07c2abcb0ea",
  "token": "9c2f0b1e-4a77-4e2b-b0c1-7f3d2a1e6b84"
}
GET/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).

FieldTypeReqDescription
idstring · pathyes
Request
curl -s https://videographer.com/api/order/{id}
Returns

200 The order status view. OrderStatus

Example response
{
  "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
}
POST/api/order/{id}/broadcast

Broadcast 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.

FieldTypeReqDescription
idstring · pathyes
tokenstringyesThe owner token from create.
Request
curl -s https://videographer.com/api/order/{id}/broadcast \
  -H 'content-type: application/json' \
  -d '{}'
Returns

200 Routed count.

Example response
{
  "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.

POST/api/account/second-shooter

Opt 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.

FieldTypeReqDescription
discoverablebooleanAbsent or false takes you OUT of the network. Nothing else in the crew API returns you until this is true.
notestringOptional. Truncated, not rejected, past the limit.
rolesarray
Request
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"]}'
Returns

200 The opt-in as stored, echoed back.

POST/api/account/availability

Mark 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.

FieldTypeReqDescription
datestringYYYY-MM-DD. Anything else is silently skipped.
statusstring`available` or `busy` set the day. Any other value CLEARS it — that is how you unmark a day.
updatesarray
Request
curl -s https://videographer.com/api/account/availability \
  -H 'content-type: application/json' \
  -d '{"updates":[{"date":"2026-08-15","status":"busy"}]}'
Returns

200 The whole map as stored, echoed back so a calendar can re-render from the server copy rather than its own guess.

GET/api/second-shooter

Search 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.

FieldTypeReqDescription
datestringyesYYYY-MM-DD.
citystringyesDefaults to your own first coverage city.
shootTypestringyes
Request
curl -s https://videographer.com/api/second-shooter
Returns

200 Matching peers. `count: 0` is a real answer, not an error.

POST/api/second-shooter/request

Ask 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.

FieldTypeReqDescription
toIdstringyesThe videographer being asked. Must be discoverable and not yourself.
datestringyesYYYY-MM-DD. Required and validated.
shootTypestring
notestringTruncated, not rejected, past the limit.
Request
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."}'
Returns

200 Created, or the same pending ask returned unchanged.

POST/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.

FieldTypeReqDescription
idstring · pathyes
actionstring · pathyes
Request
curl -s https://videographer.com/api/second-shooter/request/{id}/{action} \
  -H 'content-type: application/json' \
  -d '{}'
Returns

200 The new status, plus the requester's contact on accept.

GET/api/account/connections

Your crew requests, both directions

Inbound pending (the actionable ones), outbound pending, and accepted in both directions. Email appears only on accepted rows. Session-authenticated.

Request
curl -s https://videographer.com/api/account/connections
Returns

200 The connections view.

Account

POST/api/account/listing

Hide 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.

FieldTypeReqDescription
listedbooleanTrue republishes; false or absent hides.
Request
curl -s https://videographer.com/api/account/listing \
  -H 'content-type: application/json' \
  -d '{"listed":false}'
Returns

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.