ReelQuoteAI US API

Video-based moving inventory and quoting. A customer films a walkthrough; the API returns an item-level inventory and a firm, bookable price from the mover's own rate card. Built to be called from your own systems.

Machine-readable spec/openapi.json — OpenAPI 3.1, every response a named schema
AuthAuthorization: Bearer pk_live_… (browser-safe) or sk_live_… (server-only)
MoneyAlways integer cents + explicit currency. Never floats, never assumed USD.
ErrorsAlways { error, message }. Branch on error; a 429 carries Retry-After.
StabilityThe contract is frozen: fields are added, never renamed. Breaking changes would go to /v2.

The funnel, as calls

Ballpark → job → contact → upload → submit → poll → access → (edit items) → approve → book. Every step returns the state the next step needs.

GET /sessionResolve the key to its mover (name, branding, config).
GET /home-profilesHome sizes for the ballpark step (2br_apt, 3br_house, …).
POST /ballparkInstant estimate range from home size + addresses. Free; never bookable.
POST /jobsStart a job (the survey container). Contact details are NOT accepted here — funnel rule. Send Idempotency-Key so a retry cannot create a second job.
GET /jobsList jobs for the tenant, newest first. Secret key only. Paged: pass limit (1–100, default 25), optional status, and cursor from the previous page's next_cursor (null on the last page; a bad cursor is a 400).
GET /jobs/{id}Full job state: status, inventory, quote, access, terms, move_date.
POST /jobs/{id}/contactStore customer email + optional name + move date. Sends NOTHING — required before upload-url.
POST /jobs/{id}/upload-urlPresigned upload for the walkthrough video. Requires stored contact email. Body: { content_type, consent, size }. Send the byte size: at or above 16 MB the response carries multipart (an upload_id, a part_size and a presigned PUT per part) instead of upload, and each part is retried on its own rather than restarting the whole video. Report each part's ETag to /upload-complete. Below the threshold, or if opening a multipart upload fails, the response is a single upload.
POST /jobs/{id}/upload-completeAssemble a chunked upload. Body: { upload_id, parts: [{ part_number, etag }] }. Only after every part has been PUT; then call /submit.
POST /jobs/{id}/submitQueue the uploaded video for analysis. Poll GET /jobs/{id} for status.
POST /jobs/{id}/videoServer-side handoff: give us a public https URL of the walkthrough instead of uploading it from a browser. We fetch it (up to 500 MB, streamed) and the job runs exactly as an upload would — poll GET /jobs/{id}. Needs the stored contact email and consent, like /upload-url. Replaces /upload-url, /upload-complete and /submit for a caller that already holds the file.
POST /jobs/{id}/accessConfirm floors / elevator / stairs / carry distance for both ends.
POST /jobs/{id}/itemsEdit inventory (quantity, exclude, add by item_key). Response carries the re-priced quote.
POST /jobs/{id}/approveCustomer sign-off on the inventory, plus free-text notes.
POST /jobs/{id}/gapsRecord the customer's answer to the capture-gap prompts (inventory.gaps on GET /jobs/{id}). Body: { acknowledged: true } stamps "nothing is missing"; false clears it. The prompts themselves are recomputed from the current inventory on every read, so fixing a gap removes its prompt without this call.
POST /jobs/{id}/send-linkEmail the customer their resume link. Only on explicit request.
POST /jobs/{id}/bookRequest the booking. Idempotent per job; requires approve:true (types-name-as-signature approval of the inventory + quote, stored with a quote snapshot) and accept_terms when the mover has terms.
POST /jobs/{id}/outcomeReport actuals (hours, final cents, crew). Secret key only.
GET /jobs/{id}/cube-sheetItemized cube sheet as CSV (Excel-ready): rooms, volumes, weights, exclusions. Secret key only.
GET /jobs/{id}/date-pricesThis job's quote priced across a ~2-week window from the mover's date rules. flat=true means every day prices the same.
POST /jobs/{id}/dateSet or clear the move date; invalidates and recomputes the quote. Refused after a booking is requested.
POST /quoteDirect pricing for agents: items or total_cuft in, firm quote out. Secret key only.
GET /availabilityWhich days the mover has marked off, before a job exists. Soft: a marked day can still be requested — the mover confirms and calls first — so steer to an open day when the customer has a choice.
GET /keysList the account's API keys — live and test, active and revoked. Never a raw secret. Secret key with the keys scope.
POST /keysMint a key. The raw secret is in THIS response only. A key can never mint one broader than itself; a test key mints only test keys; a live secret key may mint the sandbox pair.
POST /keys/rotateReplace a key with its twin (same kind, mode, scopes, label). The old key keeps working until replaced.expires_at so a deploy can swap values without a gap. Needs the keys scope even to rotate itself — rotate a scoped key with a full key or from the dashboard.
POST /keys/revokeRevoke a key immediately. The only live publishable key is refused (409) — rotate it instead.
GET /webhooks/deliveriesThe delivery log: every webhook POST for 30 days, newest first, with its outcome and the exact body sent. Secret key with the keys scope.
POST /webhooks/deliveries/replayRe-send one delivery to its endpoint: the same body byte for byte (same envelope id, so your dedupe still works) under a fresh signature, with the usual retries. Returns the replay's own delivery record.
GET /catalogSearch the cube-sheet catalog (?q=sofa).
POST /places/suggestAddress autocomplete (proxied; send session_token).
POST /places/resolveResolve a suggestion to a full address (same session_token).

Job status, in funnel order

createdThe job exists. Nothing uploaded yet.
uploadingAn upload URL was issued; the video is on its way.
uploadedA chunked upload was assembled (a single PUT skips this state).
queuedPOST /submit accepted; waiting for the analysis worker.
processingThe walkthrough is being analysed. Poll GET /jobs/{id}; typically 1–4 minutes.
completeInventory ready. The firm quote is computed on the first read.
quotedA firm, bookable quote exists (set by the first GET after complete).
booked_requestedThe customer asked to book; the mover has not answered yet.
bookedThe mover confirmed the booking.
declinedThe mover turned the request down. decline_reason may say why.
errorAnalysis failed. error carries a message; upload again or contact the mover.

MCP

Endpointhttps://widget.reelquoteai.com/mcp — Streamable HTTP; one tool per call above, generated from the spec, so it can never drift from it
AuthSend your key as Authorization: Bearer sk_… on the connection. A test key gives the sandbox.
Add it to any MCP-capable assistant as a remote server:
{ "mcpServers": { "reelquoteai": { "url": "https://widget.reelquoteai.com/mcp", "headers": { "Authorization": "Bearer sk_live_…" } } } }

Acceptable use

Keys belong to one moving company and act only on that company's data — build for that company, or with its consent. Do not scrape, probe or sample the service to reproduce it, resell access, or work around rate limits; keys that do are suspended. Questions: info@reelquoteai.com.

Scopes

quotePrice without a job: /ballpark, /quote, /catalog, /home-profiles, /availability, /session, /places.
jobs:readRead jobs: GET /jobs, GET /jobs/{id}, the cube sheet, date prices.
jobs:writeCreate and drive jobs: POST /jobs and every job action except outcomes.
outcomesReport what the move actually took: POST /jobs/{id}/outcome.
keysManage the integration: list, mint, rotate and revoke API keys; read and replay webhook deliveries.
Every call above names its scope as x-scope in the spec. A key with no scope list has everything its kind allows. Test keys (pk_test_, sk_test_) create sandbox jobs: instant fixed inventory at your real rates, nothing billed, nobody emailed, webhooks say livemode:false.

Error codes

400 invalid_requestA field is missing, malformed or not allowed in this state. message says which.
400 secret_key_in_urlA secret key was sent as ?k=. Secret keys go in the Authorization header only.
401 unauthorizedNo key, an unknown key, or a revoked key.
403 secret_key_requiredThe endpoint is server-only; a publishable key cannot reach it.
403 insufficient_scopeThe key does not carry the scope this call needs (named in message). Mint one that does from the keys page or POST /keys.
409 last_publishable_keyThe only live publishable key cannot be revoked — the widget stops without one. Rotate it instead.
409 webhook_pausedThat webhook endpoint is paused; turn it back on before replaying a delivery to it.
409 video_in_flightA video is already being fetched for this job (POST /jobs/{id}/video). Poll GET /jobs/{id}.
409 video_already_suppliedThe job already has a video and has moved past upload. Start a new job for a different walkthrough.
404 not_foundNo such job for this key, or no such endpoint.
409 no_rate_cardThe mover has no active rate card, so nothing can be priced yet.
409 ballpark_not_bookableOnly a firm quote from a filmed walkthrough can be booked.
409 already_requestedThe job is already booked or awaiting the mover, so the move date can no longer change here. Contact the mover.
409 idempotency_in_progressA request with the same Idempotency-Key is still being processed. Obey Retry-After (1 s) and retry with the same key.
422 idempotency_key_reusedThat Idempotency-Key was already used with a different request body. A key names one request; use a new key for a new job.
429 rate_limitedPer-key limit (60/min publishable, 300/min secret) or the daily ballpark cap. Obey Retry-After.
500 internal_errorOur side. Safe to retry after a pause; nothing was half-applied.
501 not_implementedA documented-but-not-yet-live endpoint.
502 address_lookup_failedThe address service did not answer. Retry, or send the address as free text.
503 address_lookup_unavailableAddress autocomplete is not configured or is down. Send the address as free text.
503 upload_unavailableVideo storage is temporarily unavailable. Retry in a few minutes.

Webhooks

job.completeInventory analysed and priced.
cube_sheet.readyItemized cube sheet ready: rooms/items with volumes, weights and exclusions inline, plus the CSV URL.
job.booked_requestedCustomer asked to book.
job.bookedMover confirmed the booking.
job.declinedMover turned the booking request down. Carries reason when they gave one.
Body { id, event, created, data }, signed t=<unix>,v1=HMAC-SHA256(secret, t + "." + body). Configure endpoints from the mover dashboard.

Keys are issued at signup. Questions: info@reelquoteai.com.