Listening to Webhooks

Blueprint notifies you when clinical artifacts are ready. This is the reliable way to get
documentation into your application — unlike frontend callbacks, webhooks still arrive after the
clinician closes their browser.

Setting up

Register a callback URL for your partner organization:

curl -X PATCH https://api.blueprint.ai/v2/partners/{partnerId} \
  -H 'Content-Type: application/json' \
  -H 'Access-Token: YOUR_ACCESS_TOKEN' \
  -H 'X-API-Key: YOUR_API_KEY' \
  -d '{"callbackUrl":"https://your-app.example.com/blueprint/webhooks"}'

Read the current value with GET /v2/partners.

There is one callback URL per partner organization per environment, and all event types are
delivered to it
. There is no per-event subscription or filtering — branch on eventType in your
handler.

What you receive

A POST with these headers:

HeaderDescription
Content-TypeAlways application/json.
webhook-idStable identifier for this event, identical across retries. Dedupe on it.
webhook-timestampTime the delivery was signed, in unix seconds.
webhook-signatureOne or more space-separated v1,<base64> signatures. See Verifying the signature.
X-Blueprint-SignatureDeprecated. Hex HMAC-SHA256 of the request body keyed with your clientSecret. Still sent alongside the headers above for integrations that have not yet migrated.

The body:

{
  "eventType": "progress_note_generated",
  "timeStamp": "2026-07-29T18:04:11.522Z",
  "payload": {
    "progressNoteId": "44444444-4444-4444-4444-444444444444",
    "sessionId": "55555555-5555-5555-5555-555555555555",
    "sessionExternalId": "appt-99031",
    "clientId": "...",
    "clinicianId": "...",
    "clinicId": "...",
    "organizationId": "...",
    "progressNoteUrl": "https://api.blueprint.ai/v2/sessions/555.../progress-note"
  }
}
FieldNotes
eventTypeOne of the seven events below
timeStampISO 8601. Note the capital S.
payloadShape varies by event type

Payloads carry identifiers and a URL rather than the artifact itself. Fetch the URL with your normal
Access-Token and X-API-Key headers to get the content.

Event types

There are seven. A handler that only knows about progress notes will receive events it does not
recognize, so branch explicitly and ignore the rest.

EventFires whenResource URL in payload
progress_note_generatedA progress note has been createdprogressNoteUrl
progress_note_regeneratedA progress note has been regeneratedprogressNoteUrl
progress_note_finalizedThe clinician finalized a note. It is not locked and can still be editedprogressNoteUrl
transcript_readyThe session transcript is availabletranscriptUrl
session_transcript_errorTranscription failednone — see error
mdm_elements_identifiedMedical decision making elements are availablemdmUrl
assessment_completedA client completed an assessmentassessmentScoreUrl per score

Full payload field lists are in Webhook Events Reference.

Different events carry different URL fields. Reading payload.progressNoteUrl on a
transcript_ready event gives you undefined. Switch on eventType before reaching for a URL.

Verifying the signature

Blueprint signs every delivery using the Standard Webhooks specification, so an off-the-shelf verifier works unmodified.

Your signing key is a webhook signing secret beginning with whsec_. It is a separate credential from your clientId / clientSecret, and rotating one has no effect on the other. Ask your Blueprint contact if you do not have one yet.

Use a library

Verifiers are maintained by the Standard Webhooks project, MIT licensed, and indexed at standard-webhooks/libraries.

LanguagePackageInstall
JavaScript / TypeScriptstandardwebhooksnpm install standardwebhooks
Pythonstandardwebhookspip install standardwebhooks
Golibraries/gogo get github.com/standard-webhooks/standard-webhooks/libraries/go
PHPstandard-webhooks/standard-webhookscomposer require standard-webhooks/standard-webhooks
Rubystandardwebhooksgem install standardwebhooks
Ruststandardwebhookscargo add standardwebhooks
Java / Kotlincom.standardwebhooks:standardwebhooksMaven Central
C# / .NETStandardWebhooksNuGet — community maintained, verifies v1

The integration is two lines. The library reads the three headers itself, enforces the timestamp window, tries every signature present, and compares in constant time.

const { Webhook } = require('standardwebhooks')

const webhook = new Webhook(process.env.BLUEPRINT_WEBHOOK_SIGNING_SECRET)
const event = webhook.verify(rawBody, req.headers) // throws if invalid

Pass the raw request bytes, not a re-serialized object. Parsing and re-serializing JSON changes bytes and the signature will not match. In Express, keep a copy with body-parser's verify hook.

Verifying by hand

  1. Capture the raw request body before parsing it.
  2. Reject the delivery if webhook-timestamp falls outside a ±5 minute window.
  3. For each signing secret you hold, compute HMAC-SHA256(key, "{webhook-id}.{webhook-timestamp}.{raw body}"), where key is the base64 decoding of that secret with its whsec_ prefix removed. The key is those decoded bytes, not the whsec_… string.
  4. Split webhook-signature on spaces, keep the entries prefixed v1,, and base64-decode each digest.
  5. Accept the delivery if any presented digest matches any digest you computed. Compare in constant time.

The v1 prefix is the signature scheme version, not a key identifier. Ignore any prefix you do not recognize rather than failing the delivery, so a future scheme can ship alongside this one.

A test vector you can check your implementation against before going live is in the Partner API reference overview.

More than one signature

webhook-signature can carry more than one signature. During a rotation Blueprint signs the same delivery under both the outgoing and the incoming secret, so hold your secrets as a list and accept the delivery if any one verifies.

That overlap is what lets you install a new secret and confirm it against live traffic before the old one is retired: add the new secret, check that deliveries verify against it, and only then ask us to retire the old one. Add, never replace. Comparing against the whole header value instead of splitting it works until your first rotation, then fails every delivery.

X-Blueprint-Signature is deprecated

X-Blueprint-Signature is a hex HMAC-SHA256 of the request body alone, keyed with the UTF-8 bytes of your clientSecret. It is deprecated and should not be used by new integrations.

Nothing in it is bound to an id or a timestamp, so there is no replay window to enforce and no id to dedupe retries on. It is also keyed with your API credential, which couples two separate concerns: the signing key cannot be rotated or scoped independently of the credential you call the API with.

Migrating. The two schemes are independent headers on the same delivery, so you can cut over without a coordinated deploy:

  1. Ask your Blueprint contact for a webhook signing secret (whsec_…) if you do not already have one.
  2. Add a webhook-signature check using one of the libraries above, keeping your existing X-Blueprint-Signature check in place. Accept the delivery if either passes.
  3. Confirm against live traffic that the new check is passing.
  4. Delete the X-Blueprint-Signature check and stop reading the header.

Responding

Return a 2xx as soon as you have stored the event. Do the real work asynchronously. The per-request
timeout is 10 seconds; slower than that counts as a failure.

Retries

Your responseWhat happens
2xxDelivered. Done.
5xx, 408, 429Retried. Redelivered after a delay, for a bounded number of attempts.
Timeout or connection errorRetried, same as above.
Any other non-2xx — 400, 401, 403, 404, …Not retried. The event is dropped permanently and you are not notified.

That last row is the one to design around. A deploy that briefly returns 401, or a routing change that
returns 404, will silently lose every event that arrives during the window. Two defenses:

  • Never return a 4xx for a transient problem. If you cannot process an event right now — database
    unavailable, dependency down — return 503, not 400. A 4xx tells Blueprint the request was
    malformed and not worth retrying.
  • Return 2xx even for events you do not recognize. An unknown eventType is not an error. Log it
    and acknowledge.
async function handleEvent(event) {
  switch (event.eventType) {
    case 'progress_note_generated':
    case 'progress_note_regenerated':
      return importNote(event.payload)
    case 'progress_note_finalized':
      return markFinalized(event.payload)
    case 'transcript_ready':
      return importTranscript(event.payload)
    case 'session_transcript_error':
      return recordFailure(event.payload)
    case 'mdm_elements_identified':
      return importMdm(event.payload)
    case 'assessment_completed':
      return importScores(event.payload)
    default:
      console.warn('Unrecognized Blueprint event', event.eventType)
      return   // acknowledge anyway
  }
}

Handling duplicates

Delivery is at-least-once. webhook-id is stable across retries of the same event, so record the ones you have processed and drop repeats. Handlers should still be idempotent — use upserts — since ordering is not guaranteed.

Local development

Blueprint has to be able to reach your callback URL, so localhost will not work. Use a tunnel
(ngrok, cloudflared) and point your sandbox callbackUrl at it. Remember to change it back.

Next


Did this page help you?