Reports API
Create, read, sign reports programmatically
The Reports API lets you upload PDF radiology reports to each exam, read the active report, delete it and electronically sign it — all from your system. Signing generates a signature_id with the PDF SHA-256 hash and a public verification URL (with optional QR) so anyone can validate the document authenticity.
Upload or replace a report
Send the PDF as base64 (without the data:application/pdf;base64, prefix). If a report already exists, we replace it. This operation triggers the report.published webhook.
/api/v1/exams/{id}/reportcurl -X POST "https://cbcthub.com/api/v1/exams/$EXAM_ID/report" \
-H "Authorization: Bearer $CBCTHUB_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"pdf_base64\": \"JVBERi0xLjQKJeLjz9MKMyA...\",
\"filename\": \"informe_juan_perez.pdf\"
}"
→ {
"ok": true,
"exam_id": "exm_abc123",
"report": {
"filename": "informe_juan_perez.pdf",
"size_bytes": 412300,
"created_at": "2026-06-23T14:35:01Z"
}
}Read the active report
Returns a presigned URL of the PDF (valid for 15 minutes) and, if already signed, the signature data: signature_id, SHA-256 hash, date, public verification URL.
/api/v1/exams/{id}/reportcurl "https://cbcthub.com/api/v1/exams/$EXAM_ID/report" \
-H "Authorization: Bearer $CBCTHUB_API_KEY"
→ {
"ok": true,
"exam_id": "exm_abc123",
"report": {
"filename": "informe_juan_perez.pdf",
"size_bytes": 412300,
"url": "https://r2.cloudflarestorage.com/...?signed",
"created_at": "2026-06-23T14:35:01Z",
"signature": {
"signature_id": "sig_9z8y7x6w5v",
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"signed_at": "2026-06-23T14:36:10Z",
"verification_url": "https://cbcthub.com/v/sig_9z8y7x6w5v"
}
}
}Delete the report
Removes the active report from the exam. Idempotent: returns 200 even if there was no report. Any previous signature stays in the audit log but its public URL stops working.
/api/v1/exams/{id}/reportcurl -X DELETE "https://cbcthub.com/api/v1/exams/$EXAM_ID/report" \
-H "Authorization: Bearer $CBCTHUB_API_KEY"
→ { "ok": true }Electronically sign
Computes the PDF SHA-256 hash, records it in the signatures table and generates a public verification URL. Signing triggers the report.signed webhook. The verification URL is of the form https://cbcthub.com/v/{signature_id}.
/api/v1/exams/{id}/report/signcurl -X POST "https://cbcthub.com/api/v1/exams/$EXAM_ID/report/sign" \
-H "Authorization: Bearer $CBCTHUB_API_KEY"
→ {
"ok": true,
"signature_id": "sig_9z8y7x6w5v",
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"signed_at": "2026-06-23T14:36:10Z",
"verification_url": "https://cbcthub.com/v/sig_9z8y7x6w5v"
}Validations
- The file MUST be a valid PDF: we validate magic bytes (%PDF-). If you send anything else, we return 400 invalid_request.
- Max size: 20 MB per report. If you need more, compress images or split the document.
- POST /report is content-idempotent: uploading the same PDF twice does not duplicate nor fire the webhook twice.
- To sign, the exam must already have a report. Otherwise you get 400 invalid_request.
End-to-end example (Node.js)
Upload a PDF generated by puppeteer, electronically sign it, and obtain the public verification URL:
import { readFile } from 'node:fs/promises';
const API = 'https://cbcthub.com/api/v1';
const KEY = process.env.CBCTHUB_API_KEY!;
const examId = 'exm_abc123';
async function uploadAndSignReport(pdfPath: string) {
// 1. Leer el PDF y codificar en base64
const pdf = await readFile(pdfPath);
const pdf_base64 = pdf.toString('base64');
// 2. Subir el informe (dispara webhook report.published)
const upRes = await fetch(`${API}/exams/${examId}/report`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
pdf_base64,
filename: 'informe.pdf',
}),
});
if (!upRes.ok) throw new Error(`upload failed: ${upRes.status}`);
// 3. Firmar electrónicamente (dispara webhook report.signed)
const signRes = await fetch(`${API}/exams/${examId}/report/sign`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${KEY}` },
});
const { signature_id, verification_url, sha256 } = await signRes.json();
console.log('Informe firmado.');
console.log('URL pública:', verification_url);
console.log('Hash SHA-256:', sha256);
// verification_url se puede convertir en QR client-side con cualquier
// librería (qrcode, qrcode-generator) y agregar al PDF si lo deseas.
return { signature_id, verification_url };
}
uploadAndSignReport('./informe_juan_perez.pdf').catch(console.error);Related webhooks
If you have webhooks configured, these operations trigger events automatically:
report.published— when a PDF is uploaded or replaced.report.signed— when the report is electronically signed.
ℹ Public verification