Generating PDF condition reports
When a rental ends, a freight load arrives, or an audit lands on your desk, a JSON verification result isn't enough. You need a single artifact — branded, dated, signed — that someone can email, file, or hand to a regulator. That's what the PDF endpoints produce.
There are two of them, with the same renderer behind both:
| Endpoint | Auth | Use case |
|---|---|---|
GET /api/v1/verifications/[id]/pdf | X-API-Key | One PDF per /v1/verify call. Server-to-server access. |
GET /api/v1/inspection-sessions/[token]/pdf | Signed token | Multi-shot session reports. Safe to expose to end users. |
Both stream application/pdf directly (no redirect). An inspection's
report is rendered once it is submitted (after the Pickup comparison,
for a pickup), stored, and served from storage after that; it is also
linked from the inspection's pdf_url. The per-verification report is
rendered on every request, so cache the bytes on your side if you need
them more than once.
1. Pull a verification PDF
curl \
-H "X-API-Key: vai_your_api_key" \
https://verdictcapture.com/api/v1/verifications/ver_3aF92.../pdf \
-o condition.pdfThe endpoint streams application/pdf bytes with Content-Disposition: inline and a short private, max-age=300 cache header. There is no
redirect — always call the API endpoint, never the underlying storage
path directly.
2. Pull a session PDF (for the end user)
The token-authenticated variant takes the same token as the inspection page itself — no API key required, so it's safe to embed in the confirmation email at the end of a self-inspection flow:
curl "https://verdictcapture.com/api/v1/inspection-sessions/vai_lnk_eyJ.../pdf" \
-o my-inspection.pdfThe endpoint takes the same token as the inspection link. It keeps
working after the inspection is submitted, so the link you already
hold is enough to fetch the finished report. An expired, replaced or
tampered token gets 410. With your API key, the inspection's
pdf_url (from GET /api/v1/inspection-sessions/{id} or the
inspection.submitted webhook) is a signed link to the same file.
3. What's in the report
The renderer lives in lib/verify-ai/pdf/render.tsx and uses
@react-pdf/renderer. An inspection report is
built from the inspection's summary, so every label, status and reason
reads exactly as it does in the API, the webhook and the dashboard.
An inspection report contains:
- Header — your logo and display name, the title (Delivery
inspection, Pickup inspection, or Inspection when no
stagewas set), the inspection id, and when the report was generated. Times are printed in UTC. - Details — your
reference_id, the vehicle (year, make and model), VIN and plate from thevehicleyou sent (or, when you sent none, from a VIN or plate read off a photo), the submitted time, and the driver's name fromrecipient.name. - Outcome — Clear or Needs review, with every outcome reason.
- Odometer — the reading with its unit, and the fuel or charge level.
- Changes since delivery — pickups only. How many new damage items were found, the odometer at Delivery and Pickup with the distance driven, the fuel change, and each compared view's result (no new damage, new items, or photos not comparable). For each view with new damage, the items (description, location, severity) and the Delivery and Pickup photos side by side. When there was nothing to compare against, or the comparison failed, the section says so.
- Photos — one tile per view, labelled with the view name ("Driver side", "Odometer", "Damage close-up 1"), its result (Accepted, Flagged for review, Continued without an accepted photo, …) with the policy category, and the feedback for that photo. A required view that was never captured shows Missing.
- Damage findings — only when a photo's check reported damage. One row per finding: a number that matches the box on the photo, the view, the panel, the damage type, the severity, and the share of the panel it covers.
- Review — the staff decision (Approved or Damage confirmed), who made it and when, and the note, when the report is rendered after a decision is recorded.
- Signature — only when a signature was captured, with the signer's name and the time.
A per-verification report has the same header, the vehicle from the photo when one was read, the photo with its result and feedback, and the damage table.
4. Damage overlays
When a photo's check reported damage, each finding is drawn on the
photo as a box, numbered to match its row in the damage table, before
the photo goes into the report. Findings are stored with
bbox: [x1, y1, x2, y2] as fractions of the image size, and are
scaled to the photo as it appears in the report. Colors follow
severity:
severity: "light"→ yellowseverity: "medium"→ orange (also the default when severity is missing)severity: "severe"→ red
Findings with severity: "none" are not drawn or listed. If a box
can't be drawn, the photo is still included without it — a bad box
never blanks a photo. Photos are oriented and downsized for the page,
so a report with a dozen phone photos stays a reasonable size.
5. Branding
The header, color accents, and footer are read from
verify_ai_branding for the verification's customer:
import { getBranding } from "@/lib/verify-ai/branding";
const brand = await getBranding(customerId);
// brand.displayName, brand.logoPath, brand.primaryColor,
// brand.contactEmail, brand.pdfFooterText, brand.defaultLocalegetBranding returns the Switch Labs defaults
(DEFAULT_BRANDING) when a customer has no row — every report still
renders, it just isn't co-branded. branding.logoUrl starts null
and is resolved separately by the renderer; if no logo is uploaded the
header falls back to text-only.
Reports are written in English. recipient.locale changes the
inspection page and the invite email, not the PDF.
6. Embedding in email and the app
Two patterns work well:
Attach the PDF. Pull the bytes once on your server, attach them to the transactional email, and send. The renderer is fast, but you don't want it on the hot path of every email send — cache the result locally or trust the storage cache.
const pdf = await fetch(
`https://verdictcapture.com/api/v1/verifications/${id}/pdf`,
{ headers: { "X-API-Key": process.env.VERIFY_AI_KEY! } },
).then((r) => r.arrayBuffer());
await sendMail({
to: customer.email,
subject: `Condition report — ${rentalId}`,
attachments: [{ filename: "condition.pdf", content: Buffer.from(pdf) }],
});Link to the inspection PDF. For end-user-facing flows, put the token PDF URL straight into the email and skip the attachment. The token already authorizes it; the recipient doesn't need an account. Remember the link stops working when the token expires.
7. Inline preview vs. download
The endpoint sets Content-Disposition: inline; filename="verification-<id>.pdf" (or inspection-<id>.pdf for the
session endpoint), so browsers preview the PDF in-tab by default.
If you want a save-to-disk dialog, use an <a download> link on
your side — the browser will honor the download attribute and
override the inline disposition.
What's next
- Sending an inspection link — the upstream flow that produces multi-shot session PDFs.
- Reading VIN and license plate from a photo — populate the vehicle block so the header isn't blank.
- Verifying parked vehicles — enable damageMode on a policy to surface the damage section.