Skip to main content
Documentation

Documents

documents.readiness

Check whether a job's signing document has everything it needs
Read-onlyRead budget

REST

Shipping
POST /api/v1/documents/readiness

MCP tool

Live
documents.readiness

Exposed on: REST API · Shop MCP server · Claude connector · Dash (in-app copilot) · Zapier. Part of the Documents domain.

Operating contract

THE PRE-FLIGHT BEFORE A CUSTOMER IS HANDED THE TABLET. It resolves the shop's own template against this job's actual records and reports which values will print and which will print BLANK.

Why it matters: the renderer degrades a value it cannot find into a row of underscores, which is right for a document that has to print regardless — and exactly wrong as the first time anybody notices the VIN was never entered, because by then the customer has signed a contract that identifies their car by a blank.

ready: false with a non-empty blocking list means DO NOT SIGN YET. Those are the gaps that make the paperwork not paperwork — who is signing, and which vehicle.

recommended gaps are worth thirty seconds and do not hold the handoff. advisory gaps are genuinely unknowable at the counter right now — a pickup odometer at drop-off, an invoice total on a job with no invoice — and are reported without blame. Do not present an advisory gap as somebody's mistake.

Each gap names the RECORD AND FIELD it can be fixed in, which is the difference between 'the VIN is missing' and 'here is the box'. Filling them is a person at the counter looking at the car — this reports, and vehicles.update or customers.update is where a value actually lands.

doc_type is service_agreement (the intake paperwork) or vehicle_release (the handover). A shop that does not use one of them gets a refusal saying so rather than an empty report.

This resolves one document for one job and returns every field it carries, so nothing is truncated.

Who may call it

Permission
NoneThe signing station at `/documents/signing-station` is a counter surface open to every active member — whoever hands the customer the tablet runs this check, and that is often a technician rather than an admin. It reports gaps and writes nothing.
Plan
Every planNo plan gate. Available on every Service VIN plan.
Retries
naturalNaturally idempotent — running it twice leaves the same world as running it once. A retrying integration needs no key.
Rate class
readCounted against the read budget — the widest of the four.

Input

FieldTypeDescription
job_idrequiredstringuuid

The job whose paperwork is being checked, as returned by jobs.list.

doc_typerequiredstring

service_agreement for intake, vehicle_release for handover.

One of: service_agreement, vehicle_release

Output

FieldTypeDescription
job_idstring

doc_typestring

readyboolean

resolvednumber

totalnumber

request_statusstring | null

blockingobject[]

blocking[].labelstring

blocking[].tagstring

blocking[].fixable_fieldstring | null

recommendedobject[]

recommended[].labelstring

recommended[].tagstring

recommended[].fixable_fieldstring | null

advisoryobject[]

advisory[].labelstring

advisory[].tagstring

Examples

Built from this capability's own schema — required fields and the ones carrying a default, and nothing invented. Paste one and it validates.

curl
export SERVICEVIN_API_KEY=svk_live_…

curl -X POST https://www.servicevin.com/api/v1/documents/readiness \
  -H "Authorization: Bearer $SERVICEVIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"job_id":"9b2f1c6e-4a77-4d2b-9f31-0f1c9a8e5d20","doc_type":"service_agreement"}'

TypeScript (fetch)
const res = await fetch("https://www.servicevin.com/api/v1/documents/readiness", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SERVICEVIN_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "job_id": "9b2f1c6e-4a77-4d2b-9f31-0f1c9a8e5d20",
    "doc_type": "service_agreement"
  }),
});

// Success and failure are both envelopes. Switch on error.code, never
// on error.message — the codes are stable, the messages are for people.
const payload = await res.json();
if (!res.ok) throw new Error(payload.error.code);
const data = payload.data;

Python (requests)
import os, requests

res = requests.post(
    "https://www.servicevin.com/api/v1/documents/readiness",
    headers={"Authorization": f"Bearer {os.environ['SERVICEVIN_API_KEY']}"},
    json={
    "job_id": "9b2f1c6e-4a77-4d2b-9f31-0f1c9a8e5d20",
    "doc_type": "service_agreement"
},
    timeout=30,
)
payload = res.json()
if not res.ok:
    raise RuntimeError(payload["error"]["code"])
data = payload["data"]

MCP tools/call — https://www.servicevin.com/api/mcp
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "documents.readiness",
    "arguments": {
      "job_id": "9b2f1c6e-4a77-4d2b-9f31-0f1c9a8e5d20",
      "doc_type": "service_agreement"
    }
  }
}

Refusals

The four gates run in this order on every surface, and the order is not arbitrary — see Authentication.

StatusCodeWhen
404not_foundThe id is unknown, or the feature is not enabled for this account. Deliberately the same answer for both.
422validation_errorAn argument was wrong. The message names the field.
429rate_limitedToo many read calls. Back off and retry.
500internal_errorSomething failed on our side. Nothing was changed.