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.

POST/api/v1/exams/{id}/report
bash
curl -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.

GET/api/v1/exams/{id}/report
bash
curl "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.

DELETE/api/v1/exams/{id}/report
bash
curl -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}.

POST/api/v1/exams/{id}/report/sign
bash
curl -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:

typescript
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.publishedwhen a PDF is uploaded or replaced.
  • report.signedwhen the report is electronically signed.

Public verification

The URL https://cbcthub.com/v/{signature_id} is public: anyone with the link (patient, referring doctor, lawyer) can see the signed SHA-256 hash, the date and the signer info. If the PDF changes even one byte, the hash no longer matches and verification fails.