TruPath PCR + MAST Report API

The generator page (index.php, tpl9's own frontend) already renders a preview in the browser and can print it. This API is a separate, additive path: it re-validates the same case data server-side against the MySQL-backed reference data, re-runs the identical clinical rules, and saves a real PDF via headless Chrome.

⇩ Download Postman Collection

Authentication

Every request must include one of:

The key is defined in app/config/api_key.php (default shown above). Change this before any production deployment.

To rotate the key

  1. Copy app/config/api_key.local.php.example to app/config/api_key.local.php and set a long random value — or set the TRUPATH_API_KEY environment variable, which takes priority over both files.
  2. Update the API_KEY constant in index.php's callServerApi() function to the same value, since that page sends it as a literal (it has no server-side templating).
  3. Restart/redeploy. Requests using the old key get 401 Unauthorized.
Security model note: this is a shared-secret scheme suitable for server-to-server calls or a page you control. Because the generator page embeds the key directly in its HTML/JS, anyone who can view that page's source can see it — it does not stop the page itself from being used to submit cases. If you need to restrict who can submit cases (not just who can call the raw API), put the generator page behind the dashboard's session login, or front the API with a reverse proxy that adds its own access control. Always serve this over HTTPS in production so the key isn't sent in plaintext.

POST /api/generate-report

Headers: Content-Type: application/json, X-API-Key: <key>

The .php extension is never required on this or any other route in the app (/index, /report, /dashboard/login, etc.) — a root-level .htaccess rewrite (not a redirect) strips it internally, so POST bodies, query strings, and auth headers all pass through unchanged. The .php form (/api/generate-report.php) still works too, if you already have it hardcoded somewhere.

Request body

{
  "mode": "initial_pcr",
  "case": { "...": "see shape below" }
}
FieldDescription
mode "pcr_only" — definitive PCR-only report. Always rendered with MAST forced off regardless of any MAST order on the case (no "results pending" header). Organism/MAST validation is relaxed since MAST isn't being reported at all.

"initial_pcr" — preliminary PCR / MAST-pending report. Requires case.requisition.mastOrder to be something other than "NOT_ORDERED"; if a MAST-eligible bacterial organism is present (clinically significant, category BACTERIA, participates true, and has a standard MAST plate in Evolve Antibiotic Plate Types(3)), the header shows "PCR RESULTS ONLY / MAST RESULTS PENDING".

"final_pcr_mast" — the final report. Requires case.requisition.mastOrder to be something other than "NOT_ORDERED", requires at least one MAST-eligible bacterial organism as described above, and requires at least one S/R result in case.mastResults for every such organism.
case The same shape tpl9's own readCase() produces client-side: schemaVersion, requisition (mastOrder, billing/ICD-10 fields, etc.), patient, order, organisms (up to 12, each {organismId, load, source} where source is "PCR" or "MAST_ONLY", defaulting to "PCR"), detectedGenes (gene acronyms or ids), empiric ({drugId|genericName, displayName, instructions}), allergies (free-text strings), clinical ({pregnancy, renalIssue, hepaticIssue}), mastResults ({organismId: {drugId: "S"|"R"|"-"}}).

See reference_source/sample_imports/Sample_Complete_PCR_Case.json and Sample_Complete_PCR_MAST_Case.json for complete real examples, and app/src/CaseInput.php for the exact validation rules.

Response — success (200)

{
  "success": true,
  "message": "Report generated successfully",
  "mode": "final_pcr_mast",
  "renderer": "chrome",
  "reportId": "20260725085315_27443829",
  "pdfUrl": "{{BASE_URL}}/uploads/pdf/20260725085315_27443829.pdf",
  "htmlUrl": "{{BASE_URL}}/uploads/html/20260725085315_27443829.html",
  "getApi": "{{BASE_URL}}/api/get-report?reportId=20260725085315_27443829"
}

renderer is which engine actually produced the PDF: "chrome" (the primary path - headless Chrome printing the same self-contained HTML the in-browser preview renders, so the PDF matches it exactly) or "dompdf-fallback" (used automatically, only if Chrome can't run on this host at all - a simpler plain HTML/CSS layout, not pixel-matched to the preview, so a request never hard-fails just because Chrome is unavailable).

Response — validation error (422)

{ "success": false, "message": "A final PCR + MAST report requires a MAST order." }

Response — PDF generation failure (500)

Only happens if both renderers fail:

{ "success": false, "message": "PDF generation failed on both renderers.\nChrome: ...\nDompdf fallback also failed." }

Response — unauthorized (401)

{ "success": false, "message": "Unauthorized" }

GET /api/get-report?reportId=<id>

Headers: X-API-Key: <key>

Returns the same case payload that was submitted plus the pdfUrl, or 404 if the id doesn't exist. Report ids are validated against ^\d{14}_[0-9a-f]{8}$ before touching the filesystem, so this endpoint can't be used for path traversal.

curl examples

# Final PCR + MAST report (wrap the sample file's contents in
# {"mode":"final_pcr_mast","case": ...} first)
curl -X POST {{BASE_URL}}/api/generate-report \
  -H "Content-Type: application/json" \
  -H "X-API-Key: DF@2026:DF@2026" \
  --data @final_pcr_mast_payload.json

# Retrieve it again later
curl "{{BASE_URL}}/api/get-report?reportId=20260725085315_27443829" \
  -H "X-API-Key: DF@2026:DF@2026"

Postman collection

TruPath_API.postman_collection.json includes all three mode values (PCR Report Only, Preliminary PCR / MAST Pending, Final PCR + MAST) plus Get Report and an unauthorized-request check. Import it into Postman, then set the collection variables:

VariableDefaultSet to
baseUrl{{BASE_URL}}Your actual host, no trailing slash - use http://127.0.0.1/TruPath/tpl9 for local testing instead
apiKeyDF@2026:DF@2026Whatever app/config/api_key.php resolves to on that host

Run either "Generate..." request first — its test script captures the returned reportId into the reportId collection variable automatically, so "Get Report" works immediately afterward with no manual copy/paste.

What the API does NOT do

← Back to report generator