CBCTHubCBCTHub
PricingBlogHelp
 
 
Back to blog
webhooksapireal-timedevelopersintegration

Webhooks vs polling: building real-time exam notifications for dental imaging workflows

CBCTHub·June 24, 2026

Every dental imaging integration eventually faces the same architectural question: how does the referring dentist's app find out a CBCT scan is ready? If your answer is "we poll the API every 30 seconds", you're shipping latency, burning serverless invocations and draining mobile batteries. There's a better default — event-driven webhooks with HMAC verification and idempotent retries — and this post is the case for switching.

The two patterns in one paragraph

Polling means your code asks "is it ready yet?" on a timer. Webhooks mean CBCTHub calls your endpoint the moment an event happens. Polling is pull, webhooks are push. Both work. They have very different cost, latency and operational profiles.

Side-by-side comparison

DimensionPolling (every 30s)Webhooks
Median latency15 s (half the interval)< 1 s
Worst-case latency30 s + retry budgetseconds, with retries on top
Cloud invocations per 100 exams (assuming 10 min upload)~2,000~100
Mobile battery impactHigh — wakes the radio every intervalNone — server pushes
Behind a firewall / on a laptopWorks fineNeeds an inbound URL
DebuggabilityTrivial — replay any requestNeeds delivery logs + replay UI
BackpressureNatural — you control the rateProvider must implement queueing

For typical PMS ↔ CBCTHub flows where you want the dentist's app to update the second the exam is ready, webhooks win on every dimension that matters to end users.

When polling still makes sense

Webhooks aren't a religion. Reach for polling when:

  • You're debugging locally and don't want to tunnel ngrok/Cloudflare into your laptop.
  • Your system can't expose an inbound URL — for example, a legacy PMS deployed on a clinic LAN with no public IP and no reverse proxy budget.
  • You batch results periodically — a nightly cron that imports the day's exams works fine on polling.
  • The event is rare and you already poll on a UX trigger (e.g., when the user opens the exam list).

Everything else — real-time dentist notifications, mobile push relays, status displays in the waiting room — belongs on webhooks.

How CBCTHub signs webhooks

Every webhook delivery includes two headers:

  • X-CBCTHub-Signature: HMAC-SHA256 of the raw request body, signed with the secret you saw once when you created the subscription.
  • X-CBCTHub-Delivery: a unique ID for this delivery attempt. Use it as your idempotency key.

Verify the signature before doing anything else. If verification fails, return 401 and log the attempt — it's either a misconfigured secret or someone fishing for a vulnerability.

Node.js / Express verification

import crypto from 'crypto';
import express from 'express';

const app = express();

app.post(
  '/cbcthub/webhook',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const sig = req.header('X-CBCTHub-Signature') ?? '';
    const expected = crypto
      .createHmac('sha256', process.env.CBCTHUB_WEBHOOK_SECRET)
      .update(req.body)
      .digest('hex');

    if (
      sig.length !== expected.length ||
      !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))
    ) {
      return res.status(401).send('invalid signature');
    }

    const deliveryId = req.header('X-CBCTHub-Delivery');
    const event = JSON.parse(req.body.toString());

    // Idempotency — return 200 if we've seen this delivery before
    if (await deliveries.has(deliveryId)) return res.status(200).send('duplicate');
    await deliveries.add(deliveryId);

    await handleEvent(event); // your business logic
    return res.status(200).send('ok');
  }
);

Python / FastAPI verification

import hmac, hashlib, os
from fastapi import FastAPI, Request, HTTPException

app = FastAPI()
SECRET = os.environ['CBCTHUB_WEBHOOK_SECRET'].encode()

@app.post('/cbcthub/webhook')
async def webhook(req: Request):
    body = await req.body()
    sig = req.headers.get('x-cbcthub-signature', '')
    expected = hmac.new(SECRET, body, hashlib.sha256).hexdigest()

    if not hmac.compare_digest(sig, expected):
        raise HTTPException(401, 'invalid signature')

    delivery_id = req.headers.get('x-cbcthub-delivery')
    event = await req.json()

    if await deliveries.contains(delivery_id):
        return {'status': 'duplicate'}
    await deliveries.add(delivery_id)

    await handle_event(event)
    return {'status': 'ok'}

Idempotency: the rule you cannot skip

CBCTHub retries deliveries that don't receive a 2xx response, with exponential backoff for up to 24 hours. That means your endpoint will receive the same event more than once eventually, either because a 5xx leaked or because the response timed out. Use X-CBCTHub-Delivery as a deduplication key in Redis, a database table, or even an LRU cache, and return 200 on the second attempt without re-running side effects.

Test mode: validate without touching production

Generate a cbct_test_ API key, register a webhook against it, and trigger a test event from the dashboard. Events fired with a test key carry "livemode": false in their payload, so your code can route them to a staging database while still running the same handler. This is the safest way to ship a webhook integration without risking a real exam delivery going to the wrong inbox.

Pattern: webhooks → durable queue → workers

The most robust production pattern keeps your webhook handler small: verify signature, enqueue, return 200. A separate worker pool consumes the queue and does the slow work. This isolates webhook acknowledgement from your business logic, so a database hiccup never causes CBCTHub to back off.

Concrete stacks:

  • Vercel: webhook route → Inngest or QStash → background functions.
  • AWS: API Gateway → SQS → Lambda consumers.
  • Self-hosted: Express → Redis stream → BullMQ workers.
  • Cloudflare: Worker → Queue → consumer Worker.

When to choose what

Default to webhooks for any real-time, user-facing event. Keep polling around for nightly imports, debugging tools and legacy on-prem systems that can't accept inbound traffic. The hybrid is fine — many production integrations use both, with webhooks as the primary path and a daily reconciliation poll as a safety net.

Wrap up

Polling is the easy first answer and the wrong long-term one. HMAC-verified webhooks with idempotent handlers and a small queue cost less, scale better and give end users a real-time experience. The CBCTHub webhook delivery system is built for this — automatic retries, replay UI, livemode separation — so the only thing left for you to do is build the receiver.

Read the webhooks reference · Create a free account

Try free viewerSee solutions

Try CBCTHub for free

Upload, view, and share DICOM scans in the cloud. Nothing to install.

Create free account

Related articles

How to integrate your dental practice management software with CBCTHub: a complete API tutorial

Step-by-step guide to integrating a dental PMS with the CBCTHub REST API. Upload exams, embed the 3D viewer, and automate delivery in Node, Python and PHP.

HIPAA-compliant dental imaging APIs: security checklist for US healthcare integrations

A 10-point HIPAA security checklist for any dental imaging API integration in the US. Encryption, BAAs, audit logs, breach notification, patient rights.

Best CBCT Viewers 2026: Complete Comparison of 10 Dental Imaging Software Options

In-depth comparison of the best CBCT viewers for dental imaging in 2026. Cloud-based and desktop options, pricing, features, and best-fit scenarios.

CBCTHubCBCTHub

Digital CBCT delivery. 100% local processing. No CDs, ever.

Download on theApp Store
Get it onGoogle Play

Solutions

Imaging centersDental radiologistsOnline CBCT viewer

Product

FeaturesPricingBlogAlternativesLearnEducationNewDevelopersAPIDemo

Support

Help centerFAQContactsoporte@cbcthub.comStatus+56 9 7632 9096

Company

AboutSecurityTerms of servicePrivacy policy

By country

United StatesUnited KingdomCanadaAustralia
HIPAA-readyGDPRLGPDLey 21.719

© 2026 CBCTHub. All rights reserved.

AppLab Software LLC · 1021 E Lincolnway, Cheyenne, WY 82001