Endpoint reference
All REST endpoints of API v1
Complete reference for every endpoint in the v1 API. Base URL: https://cbcthub.com/api/v1
/api/v1/meReturns your account info (plan, storage, exam count). Useful to verify the key works.
{
"user_id": "...",
"email": "soporte@miclinica.com",
"plan": "pro",
"scopes": ["exams:read", "exams:write"],
"storage": { "used_bytes": 41200000000, "limit_bytes": 375809638400, "used_percent": 11 },
"exams": { "count": 110, "limit": 999999 }
}/api/v1/examsCreates an exam and returns presigned URLs for direct upload to R2.
| Field | Type | Required | Notes |
|---|---|---|---|
| name | string | ✓ | Internal exam name (max 200 chars). |
| exam_type | string | — | cbct (default), radiografia, mesh, atm. Radiografia requires Pro. |
| patient_name | string | — | Patient name. Shown in viewer. |
| patient_id | string | — | SSN, ID number, medical record number. |
| birth_date | string ISO | — | YYYY-MM-DD. |
| reason | string | — | Reason / clinical findings requested. |
| expiration_days | number | — | Days until expiry. null = never. |
| upload_mode | string | — | "files" (default) or "zip". In zip mode you get 1 URL, then call /process-zip. |
| files | array | — | [{ name, size }]. Required if upload_mode="files". Min 1, max 5000. |
| zip_size_bytes | number | — | Required if upload_mode="zip". Max 1 GB (1073741824). |
/api/v1/exams?limit=20&offset=0List your account exams, paginated. Default limit=20, max 100.
/api/v1/exams/{id}Single exam details. Includes share_url, viewer_url, share_password and public_page_pin.
/api/v1/exams/{id}/confirmMarks exam as ready after files are uploaded (files mode). Idempotent.
/api/v1/exams/{id}/process-zipFor exams created with upload_mode="zip": server-side streams the staging ZIP, uploads each DICOM to R2 and marks the exam as ready. Idempotent. Limits: 1 GB ZIP, 5000 files, 5 GB expanded, 1.6 TB per individual file, 300 s timeout.
/api/v1/exams/{id}Deletes the exam. R2 files are cleaned up in background. Idempotent: returns already_deleted=true if the exam was already gone. Scope: exams:write.
curl -X DELETE "https://cbcthub.com/api/v1/exams/EXAM_ID" \
-H "Authorization: Bearer $CBCTHUB_KEY"
# → { "deleted": true, "exam_id": "EXAM_ID" }/api/v1/exams/{id}Partial update — only the fields you include are modified. Allowed: name, patient_name, patient_id, birth_date, reason, expiration_days, radiograph_subtype (radiografia only). Cannot change exam_type, share_token, storage or counters.
curl -X PATCH "https://cbcthub.com/api/v1/exams/EXAM_ID" \
-H "Authorization: Bearer $CBCTHUB_KEY" \
-H "Content-Type: application/json" \
-d '{"patient_name":"Juan Pérez","expiration_days":90}'/api/v1/exams/{id}/shareEmail the exam to a recipient. Body: { to, cc?, message?, locale? }. In sandbox no real email is sent (sent=false, reason=test_mode_no_send). For radiographs the images are attached; for CBCT the email contains the viewer link.
curl -X POST "https://cbcthub.com/api/v1/exams/EXAM_ID/share" \
-H "Authorization: Bearer $CBCTHUB_KEY" \
-H "Content-Type: application/json" \
-d '{"to":"paciente@example.com","locale":"es"}'/api/v1/exams/{id}/extrasStart an extra take for CBCT/ATM (multi-take). Returns presigned URLs for each file. Body: { label, files: [{name, size}] }. Max 2 extras per exam (3 takes total). Not for radiographs. Scope: exams:write.
curl -X POST "https://cbcthub.com/api/v1/exams/EXAM_ID/extras" \
-H "Authorization: Bearer $CBCTHUB_KEY" \
-H "Content-Type: application/json" \
-d '{
"label": "Segunda toma post-ortodoncia",
"files": [{ "name": "IM0001.dcm", "size": 524288 }]
}'
# → { "index": 1, "upload_urls": [...], "confirm_url": ".../extras/1/confirm" }/api/v1/exams/{id}/extras/{index}/confirmConfirms the extra take finished uploading. Verifies the files in R2 and updates extra_series + storage. Optional body: { label }. Idempotent. Scope: exams:write.
curl -X POST "https://cbcthub.com/api/v1/exams/EXAM_ID/extras/1/confirm" \
-H "Authorization: Bearer $CBCTHUB_KEY" \
-H "Content-Type: application/json" \
-d '{"label":"Segunda toma post-ortodoncia"}'/api/v1/exams/{id}/access-pinReturns the current PIN + info about the clinic public page. Useful to show the PIN to the patient without going through the dashboard. Scope: exams:read.
curl "https://cbcthub.com/api/v1/exams/EXAM_ID/access-pin" \
-H "Authorization: Bearer $CBCTHUB_KEY"
# → { "pin": "482931", "clinic_slug": "miclinica", "public_url": "https://cbcthub.com/centro/miclinica" }/api/v1/exams/{id}/access-pinRotates the PIN: generates a new one and immediately invalidates the old one. Empty body. Use this if a patient leaked the PIN or after a security incident. Scope: exams:write.
curl -X POST "https://cbcthub.com/api/v1/exams/EXAM_ID/access-pin" \
-H "Authorization: Bearer $CBCTHUB_KEY"
# → { "pin": "739184", "rotated_at": "2026-06-27T18:20:11Z" }/api/v1/exams/{id}/embedReturns the embed state (enabled, token, snippet, view_count). Scope: exams:read.
curl "https://cbcthub.com/api/v1/exams/EXAM_ID/embed" \
-H "Authorization: Bearer $CBCTHUB_KEY"/api/v1/exams/{id}/embedEnables the embed (generates a token if missing). Returns embed_url and iframe_snippet ready to paste. Optional body: { title, show_year, show_study_type }.
curl -X POST "https://cbcthub.com/api/v1/exams/EXAM_ID/embed" \
-H "Authorization: Bearer $CBCTHUB_KEY" \
-H "Content-Type: application/json" \
-d '{"title":"Caso 12345","show_year":true}'/api/v1/exams/{id}/embedRevokes the embed: deletes the token and breaks the existing URL. To reactivate, POST again (returns a fresh token). Scope: exams:write.
curl -X DELETE "https://cbcthub.com/api/v1/exams/EXAM_ID/embed" \
-H "Authorization: Bearer $CBCTHUB_KEY"/api/v1/exams/{id}/thumbnailDownloads the exam thumbnail (JPEG/PNG) as binary. Returns 404 with thumbnail_not_ready if not generated yet. Ideal for showing thumbnails in your UI. Scope: exams:read.
curl -o thumb.jpg "https://cbcthub.com/api/v1/exams/EXAM_ID/thumbnail" \
-H "Authorization: Bearer $CBCTHUB_KEY"/api/v1/exams/{id}/patient-examsLists the other exams of the same patient (match by patient_id; fallback to normalized name). Useful to show history or enable comparators. Scope: exams:read.
curl "https://cbcthub.com/api/v1/exams/EXAM_ID/patient-exams" \
-H "Authorization: Bearer $CBCTHUB_KEY"
# → { "exams": [{ "id": "...", "name": "...", "exam_type": "cbct", "created_at": "..." }] }Informed consent
Full legal flow: initialization by country/locale, in-person signing (patient present with a tablet) or by email (link sent to the patient), download of the signed PDF with SHA-256 and a public verification QR.
/api/v1/exams/{id}/consentReturns the current consent state (status, signing_method, signed_at, hash). If not yet initialized, returns { consent: null }. Scope: exams:read.
curl "https://cbcthub.com/api/v1/exams/EXAM_ID/consent" \
-H "Authorization: Bearer $CBCTHUB_KEY"/api/v1/exams/{id}/consentInitializes (or re-initializes if pending) the consent. Optional body: { country_code, locale }. Returns 409 if already signed. Scope: exams:write.
curl -X POST "https://cbcthub.com/api/v1/exams/EXAM_ID/consent" \
-H "Authorization: Bearer $CBCTHUB_KEY" \
-H "Content-Type: application/json" \
-d '{"country_code":"CL","locale":"es"}'/api/v1/exams/{id}/consent/sign-inpersonIn-person signing. Body: { signer_name, signer_rut, patient_birth_date?, signature_image (PNG base64), accepted: true }. The server generates the signed PDF and computes SHA-256. Scope: exams:write.
curl -X POST "https://cbcthub.com/api/v1/exams/EXAM_ID/consent/sign-inperson" \
-H "Authorization: Bearer $CBCTHUB_KEY" \
-H "Content-Type: application/json" \
-d '{
"signer_name": "Juan Perez",
"signer_rut": "12345678-9",
"signature_image": "iVBORw0KGgoAAAANSUhEUgAA...",
"accepted": true
}'/api/v1/exams/{id}/consent/emailEmails the signing link to the patient. Body: { to, patient_name? }. In sandbox no real email is sent. Returns sign_url so you can also render it as a QR. Scope: exams:write.
curl -X POST "https://cbcthub.com/api/v1/exams/EXAM_ID/consent/email" \
-H "Authorization: Bearer $CBCTHUB_KEY" \
-H "Content-Type: application/json" \
-d '{"to":"paciente@example.com","patient_name":"Juan Perez"}'/api/v1/exams/{id}/consent/pdf?type=blank|signedDownloads the consent PDF as binary. type=blank returns the unsigned document (useful for printing). type=signed requires the consent to be signed. Scope: exams:read.
curl -o consent.pdf \
"https://cbcthub.com/api/v1/exams/EXAM_ID/consent/pdf?type=signed" \
-H "Authorization: Bearer $CBCTHUB_KEY"Radiology reports
Endpoints to upload, read, delete and electronically sign PDF reports. Full details in the Reports API section.
/api/v1/exams/{id}/reportUpload or replace the report PDF. Body: { pdf_base64, filename? }.
/api/v1/exams/{id}/reportReturns the PDF presigned URL + signature data if signed.
/api/v1/exams/{id}/reportDeletes the report. Idempotent: returns 200 even if it does not exist.
/api/v1/exams/{id}/report/signElectronically signs the report. Returns signature_id and public verification URL.
Webhooks
Endpoints to manage webhook subscriptions. Full details (events, HMAC signature, retries) in the Webhooks section.
/api/v1/webhooksCreates a subscription. Returns the secret ONLY ONCE.
/api/v1/webhooksLists all account subscriptions.
/api/v1/webhooks/{id}Single subscription details.
/api/v1/webhooks/{id}Updates events, description or enabled.
/api/v1/webhooks/{id}Deletes the subscription. Cannot be reactivated.
/api/v1/webhooks/{id}/testSends a test ping to the configured endpoint.
/api/v1/webhooks/{id}/deliveriesAudit log of the last 50 deliveries (status, response, retry count).
Referrers
CRUD over the directory of referring dentists. New scopes: referrers:read and referrers:write. Sandbox: reads return an empty list and writes return a mock with id "rfr_test_*" without persisting, so you can test the integration without touching live data.
/api/v1/referrersLists every referrer of the account, sorted alphabetically by name. Scope: referrers:read.
curl "https://cbcthub.com/api/v1/referrers" \
-H "Authorization: Bearer $CBCTHUB_KEY"
# → { "referrers": [{ "id": "rfr_...", "name": "Dra. Maria Lopez", "email": "...", "specialty": "..." }] }/api/v1/referrersCreates a referrer. Body: { name (required), email?, phone?, specialty? }.
curl -X POST "https://cbcthub.com/api/v1/referrers" \
-H "Authorization: Bearer $CBCTHUB_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Dra. Maria Lopez","email":"maria@example.com","specialty":"Endodoncia"}'/api/v1/referrers/{id}Single referrer detail. Scope: referrers:read.
/api/v1/referrers/{id}Partial update — only the included fields are touched. Scope: referrers:write.
curl -X PATCH "https://cbcthub.com/api/v1/referrers/rfr_xyz" \
-H "Authorization: Bearer $CBCTHUB_KEY" \
-H "Content-Type: application/json" \
-d '{"phone":"+56 9 1234 5678"}'/api/v1/referrers/{id}Deletes the referrer. Idempotent: returns already_deleted=true if not found. Scope: referrers:write.
curl -X DELETE "https://cbcthub.com/api/v1/referrers/rfr_xyz" \
-H "Authorization: Bearer $CBCTHUB_KEY"Report templates
CRUD over plate templates for radiology reports. Templates with is_default=true are system-provided, visible to every account, and cannot be edited or deleted. New scopes: templates:read and templates:write. Sandbox: GET returns only the defaults and writes are no-ops (mock with id "tpl_test_*").
/api/v1/templatesLists every visible template (system defaults + the user's own). Scope: templates:read.
curl "https://cbcthub.com/api/v1/templates" \
-H "Authorization: Bearer $CBCTHUB_KEY"/api/v1/templatesCreates a user template. Body: { name, description?, pages: PageDef[] }. Each page: { name?, slots: [{ x, y, w, h, label? }] } with 0..1 coordinates. Scope: templates:write.
curl -X POST "https://cbcthub.com/api/v1/templates" \
-H "Authorization: Bearer $CBCTHUB_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Periapical 14 placas",
"pages": [{
"name": "Pagina 1",
"slots": [
{ "x": 0.05, "y": 0.05, "w": 0.2, "h": 0.3, "label": "11" },
{ "x": 0.30, "y": 0.05, "w": 0.2, "h": 0.3, "label": "12" }
]
}]
}'/api/v1/templates/{id}Single template detail (system or own). Scope: templates:read.
curl "https://cbcthub.com/api/v1/templates/tpl_xyz" \
-H "Authorization: Bearer $CBCTHUB_KEY"/api/v1/templates/{id}Updates an owned template. System defaults return 400. Scope: templates:write.
curl -X PATCH "https://cbcthub.com/api/v1/templates/tpl_xyz" \
-H "Authorization: Bearer $CBCTHUB_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Periapical 14 placas v2"}'/api/v1/templates/{id}Deletes an owned template. Idempotent. System defaults return 400. Scope: templates:write.
curl -X DELETE "https://cbcthub.com/api/v1/templates/tpl_xyz" \
-H "Authorization: Bearer $CBCTHUB_KEY"Implant catalog
Global implant model catalog (brand, product_line, length and diameter). Read-only. Reference resource, not per-user: uses the exams:read scope. Sandbox: the catalog is identical to live.
/api/v1/implantsWithout params returns the list of brands with count(*). Filters: ?brand=Neodent (models of that brand), ?search=helix (free text on brand+product_line), ?length=11.5&diameter=4 (inverse lookup by size; also returns near_matches within ±0.5 mm). Supports ?limit= and ?offset=.
curl "https://cbcthub.com/api/v1/implants?brand=Neodent&limit=20" \
-H "Authorization: Bearer $CBCTHUB_KEY"
# Busqueda por dimension
curl "https://cbcthub.com/api/v1/implants?length=11.5&diameter=4" \
-H "Authorization: Bearer $CBCTHUB_KEY"Support tickets
Programmatically create and read support tickets. New scopes: support:read and support:write. Creating a ticket in live sends an email to the support team and a confirmation email to the requester. Sandbox: NO persistence, NO emails, returns a mock with id "tkt_test_*" and test_mode=true.
/api/v1/support/ticketsLists the 50 most recent tickets of the account (configurable via ?limit=). Scope: support:read.
curl "https://cbcthub.com/api/v1/support/tickets?limit=20" \
-H "Authorization: Bearer $CBCTHUB_KEY"/api/v1/support/ticketsCreates a ticket. Body: { subject, message, category?, exam_id? }. Valid categories: general, bug, billing, feature_request, account, viewer.
curl -X POST "https://cbcthub.com/api/v1/support/tickets" \
-H "Authorization: Bearer $CBCTHUB_KEY" \
-H "Content-Type: application/json" \
-d '{
"subject":"Mi examen no se subio",
"message":"Subi un ZIP de 800 MB y aparece pending desde hace 2 horas.",
"category":"bug",
"exam_id":"b1f7c9c6-8c10-4d27-8d80-93e1f1a5d4af"
}'/api/v1/support/tickets/{id}Ticket detail + chronological list of admin replies. Scope: support:read.
curl "https://cbcthub.com/api/v1/support/tickets/tkt_xyz" \
-H "Authorization: Bearer $CBCTHUB_KEY"
# → { "ticket": {...}, "replies": [{ "body": "...", "created_at": "..." }] }Activity and notifications
Read the account activity log and generated notifications (support replies, system events). New scopes: account:read and account:write. Sandbox: returns empty lists.
/api/v1/activityPaginated activity log (descending). Query: ?limit=50 (1-100), ?before=ISO_DATE (cursor — use next_cursor from the response), ?action=exam.created (optional filter).
curl "https://cbcthub.com/api/v1/activity?limit=20&action=exam.created" \
-H "Authorization: Bearer $CBCTHUB_KEY"/api/v1/notificationsLists generic notifications + unread_count. Query: ?unread_only=true, ?limit=30. Does NOT include exam-view events (those flow through webhooks). Scope: account:read.
curl "https://cbcthub.com/api/v1/notifications?unread_only=true&limit=30" \
-H "Authorization: Bearer $CBCTHUB_KEY"
# → { "notifications": [...], "unread_count": 4 }/api/v1/notifications/{id}/mark-readMarks a notification as read. Idempotent. Scope: account:write.
curl -X POST "https://cbcthub.com/api/v1/notifications/ntf_xyz/mark-read" \
-H "Authorization: Bearer $CBCTHUB_KEY"/api/v1/notifications/mark-all-readMarks every unread notification as read and returns marked_count. Scope: account:write.
curl -X POST "https://cbcthub.com/api/v1/notifications/mark-all-read" \
-H "Authorization: Bearer $CBCTHUB_KEY"
# → { "marked_count": 4 }Try these endpoints live
GET /api/v1/exams
/api/v1/examsScope: exams:readTry it nowList your account exams, paginated (newest first).
Paste an API key (live or test) created from Settings → API. Create or manage keys →
Query parameters
Request
GET https://cbcthub.com/api/v1/exams?limit=5&offset=0Your API key is stored only in this browser for the current session. It never goes back to CBCTHub outside the request you fire, and it is dropped when you close the tab.
POST /api/v1/exams
/api/v1/examsScope: exams:writeTry it nowCreate an exam and get presigned URLs for direct R2 upload.
Paste an API key (live or test) created from Settings → API. Create or manage keys →
Request
POST https://cbcthub.com/api/v1/examsYour API key is stored only in this browser for the current session. It never goes back to CBCTHub outside the request you fire, and it is dropped when you close the tab.
GET /api/v1/exams/{id}
/api/v1/exams/{id}Scope: exams:readTry it nowSingle exam details. You need an id from your account (get one from the list playground above).
Paste an API key (live or test) created from Settings → API. Create or manage keys →
Path parameters
Request
GET https://cbcthub.com/api/v1/exams/{id}Your API key is stored only in this browser for the current session. It never goes back to CBCTHub outside the request you fire, and it is dropped when you close the tab.
POST /api/v1/webhooks
/api/v1/webhooksScope: webhooks:writeTry it nowCreate a webhook subscription. The response returns the secret ONLY ONCE.
Paste an API key (live or test) created from Settings → API. Create or manage keys →
Request
POST https://cbcthub.com/api/v1/webhooksYour API key is stored only in this browser for the current session. It never goes back to CBCTHub outside the request you fire, and it is dropped when you close the tab.
POST /api/v1/webhooks/{id}/test
/api/v1/webhooks/{id}/testScope: webhooks:writeTry it nowSends a test ping (test.ping) to the configured endpoint.
Paste an API key (live or test) created from Settings → API. Create or manage keys →
Path parameters
Request
POST https://cbcthub.com/api/v1/webhooks/{id}/testYour API key is stored only in this browser for the current session. It never goes back to CBCTHub outside the request you fire, and it is dropped when you close the tab.