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:
| Header | Description |
|---|---|
Content-Type | Always application/json. |
webhook-id | Stable identifier for this event, identical across retries. Dedupe on it. |
webhook-timestamp | Time the delivery was signed, in unix seconds. |
webhook-signature | One or more space-separated v1,<base64> signatures. See Verifying the signature. |
X-Blueprint-Signature | Deprecated. 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"
}
}| Field | Notes |
|---|---|
eventType | One of the seven events below |
timeStamp | ISO 8601. Note the capital S. |
payload | Shape 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.
| Event | Fires when | Resource URL in payload |
|---|---|---|
progress_note_generated | A progress note has been created | progressNoteUrl |
progress_note_regenerated | A progress note has been regenerated | progressNoteUrl |
progress_note_finalized | The clinician finalized a note. It is not locked and can still be edited | progressNoteUrl |
transcript_ready | The session transcript is available | transcriptUrl |
session_transcript_error | Transcription failed | none — see error |
mdm_elements_identified | Medical decision making elements are available | mdmUrl |
assessment_completed | A client completed an assessment | assessmentScoreUrl per score |
Full payload field lists are in Webhook Events Reference.
Different events carry different URL fields. Reading
payload.progressNoteUrlon a
transcript_readyevent gives youundefined. Switch oneventTypebefore 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.
| Language | Package | Install |
|---|---|---|
| JavaScript / TypeScript | standardwebhooks | npm install standardwebhooks |
| Python | standardwebhooks | pip install standardwebhooks |
| Go | libraries/go | go get github.com/standard-webhooks/standard-webhooks/libraries/go |
| PHP | standard-webhooks/standard-webhooks | composer require standard-webhooks/standard-webhooks |
| Ruby | standardwebhooks | gem install standardwebhooks |
| Rust | standardwebhooks | cargo add standardwebhooks |
| Java / Kotlin | com.standardwebhooks:standardwebhooks | Maven Central |
| C# / .NET | StandardWebhooks | NuGet — 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 invalidPass 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
- Capture the raw request body before parsing it.
- Reject the delivery if
webhook-timestampfalls outside a ±5 minute window. - For each signing secret you hold, compute
HMAC-SHA256(key, "{webhook-id}.{webhook-timestamp}.{raw body}"), wherekeyis the base64 decoding of that secret with itswhsec_prefix removed. The key is those decoded bytes, not thewhsec_…string. - Split
webhook-signatureon spaces, keep the entries prefixedv1,, and base64-decode each digest. - 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 deprecatedX-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:
- Ask your Blueprint contact for a webhook signing secret (
whsec_…) if you do not already have one. - Add a
webhook-signaturecheck using one of the libraries above, keeping your existingX-Blueprint-Signaturecheck in place. Accept the delivery if either passes. - Confirm against live traffic that the new check is passing.
- Delete the
X-Blueprint-Signaturecheck 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 response | What happens |
|---|---|
| 2xx | Delivered. Done. |
| 5xx, 408, 429 | Retried. Redelivered after a delay, for a bounded number of attempts. |
| Timeout or connection error | Retried, 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
eventTypeis 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
- Webhook Events Reference — every event's payload fields
- Errors — fetching artifacts that are not ready yet
- Pagination & Limits — idempotency in the rest of the API
Updated 2 days ago
