Overview
The Scribeberry Widget allows you to embed our full AI medical scribe application directly within your EMR system. Your users get access to:
- 🎤 Voice recording and transcription
- 🤖 AI-powered clinical note generation
- 📝 Customizable templates
- 📤 Push notes directly back to your EMR
How It Works
- 1. Your backend requests a token from Scribeberry (one API call with your partner key)
- 2. Your frontend embeds the widget using that token (simple iframe)
- 3. Optionally send patient context to pre-fill data (postMessage API)
- 4. Receive generated notes back (postMessage API)
Requirements
- ✓ Your EMR must be web-based (cloud)
- ✓ HTTPS is required
- ✓ Modern browser support (Chrome, Firefox, Safari, Edge)
Quick Start
Step 1: Get Your API Key
Contact Scribeberry partnerships to receive your partner API key:
Email: hello@scribeberry.com
You will receive a key in this format: your-partner-id.secret-key-here
Important: You must also provide Scribeberry with your EMR's domain(s) for secure postMessage communication (e.g., https://your-emr.com). This is required for security.
Step 2: Backend - Generate Token
Add an endpoint that generates Scribeberry tokens for your authenticated users:
Node.js / Express Example:
app.get('/api/scribeberry/token', async (req, res) => {
const user = req.user;
const response = await fetch('https://app.scribeberry.com/api/widget/token', {
method: 'POST',
headers: {
'X-Partner-Key': process.env.SCRIBEBERRY_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
userEmail: user.email,
emrUserId: user.id,
}),
});
const result = await response.json();
// Forward the response (includes { success, data: { token, expiresAt } })
res.status(response.status).json(result);
});
Python / Flask Example:
@app.route('/api/scribeberry/token')
def get_scribeberry_token():
user = get_current_user()
response = requests.post(
'https://app.scribeberry.com/api/widget/token',
headers={
'X-Partner-Key': os.environ['SCRIBEBERRY_API_KEY'],
'Content-Type': 'application/json',
},
json={
'userEmail': user.email,
'emrUserId': user.id,
}
)
# Forward the response (includes { success, data: { token, expiresAt } })
return jsonify(response.json()), response.status_code
C# / .NET Example:
[HttpGet("api/scribeberry/token")]
public async Task<IActionResult> GetScribeberryToken()
{
var user = GetCurrentUser();
var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-Partner-Key", _config["ScribeberryApiKey"]);
var response = await client.PostAsJsonAsync(
"https://app.scribeberry.com/api/widget/token",
new {
userEmail = user.Email,
emrUserId = user.Id
}
);
// Forward the response (includes { success, data: { token, expiresAt } })
var content = await response.Content.ReadAsStringAsync();
return Content(content, "application/json");
}
Step 3: Frontend - Embed Widget
<!-- Add the iframe where you want Scribeberry to appear -->
<iframe
id="scribeberry-widget"
style="width: 400px; height: 600px; border: none; border-radius: 8px;"
allow="microphone"
></iframe>
<script>
async function loadScribeberry() {
const response = await fetch('/api/scribeberry/token');
const result = await response.json();
// API returns { success: true, data: { token, expiresAt } }
const { token } = result.data;
document.getElementById('scribeberry-widget').src =
`https://app.scribeberry.com/widget?token=${token}`;
}
loadScribeberry();
</script>
That's it! Your basic integration is complete.
Authentication
Token Endpoint
URL: POST https://app.scribeberry.com/api/widget/token
| Header |
Required |
Description |
X-Partner-Key |
Yes |
Your partner API key |
Content-Type |
Yes |
Must be application/json |
Request Body:
{
"userEmail": "doctor@clinic.com",
"emrUserId": "user-123"
}
| Field |
Type |
Required |
Description |
userEmail |
string |
Yes |
User's email address |
emrUserId |
string |
Yes |
Your internal user ID |
Success Response (200):
{
"success": true,
"data": {
"token": "eyJhbGciOiJIUzI1NiIs...",
"expiresAt": "2024-01-16T12:00:00.000Z"
}
}
Error Response Format:
{
"success": false,
"error": "Error message here",
"code": "ERROR_CODE"
}
Error Responses:
| Status |
Code |
Error |
Cause |
| 401 |
MISSING_API_KEY |
Missing X-Partner-Key header |
Header not provided |
| 401 |
INVALID_API_KEY |
Invalid API key |
Wrong or inactive key |
| 400 |
INVALID_REQUEST |
Invalid request body |
Missing/invalid fields |
| 503 |
SERVICE_UNAVAILABLE |
Widget service not configured |
Server not configured |
| 500 |
INTERNAL_ERROR |
Internal server error |
Server error |
Token Expiration: Tokens expire after 24 hours by default. When a token expires, users will see a "Session Expired" message. Generate a new token when this happens.
Embedding the Widget
Basic Embed
<iframe
id="scribeberry-widget"
src="https://app.scribeberry.com/widget?token=YOUR_TOKEN"
style="width: 400px; height: 600px; border: none;"
></iframe>
Recommended Styling
<iframe
id="scribeberry-widget"
src="https://app.scribeberry.com/widget?token=YOUR_TOKEN"
style="
width: 100%;
max-width: 450px;
height: 700px;
border: none;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
"
allow="microphone"
></iframe>
Important: Include allow="microphone" to enable voice recording.
Responsive Sizing
<style>
.scribeberry-container {
position: fixed;
bottom: 20px;
right: 20px;
width: 400px;
height: 600px;
z-index: 9999;
}
@media (max-width: 768px) {
.scribeberry-container {
width: 100%;
height: 100%;
bottom: 0;
right: 0;
}
}
</style>
<div class="scribeberry-container">
<iframe
id="scribeberry-widget"
src="https://app.scribeberry.com/widget?token=YOUR_TOKEN"
style="width: 100%; height: 100%; border: none;"
allow="microphone"
></iframe>
</div>
Iframe Styling
✓ What You CAN Style:
You have full control over the iframe container styling:
- Size: width, height, max-width, min-width
- Position: position, top, bottom, left, right, z-index
- Appearance: border, border-radius, box-shadow, background
- Layout: margin, padding, display, flex properties
- Responsive: Media queries for different screen sizes
⚠ What You CANNOT Style:
The internal widget content has a fixed appearance:
- Widget UI colors, fonts, and layout
- Button styles and icons
- Internal spacing and components
Note: Custom theming for widget content is on our roadmap.
Toggle Button Pattern
Example of showing/hiding the widget with a button:
<button id="toggle-scribeberry" onclick="toggleWidget()">
🎤 Open Scribeberry
</button>
<div id="scribeberry-container" style="display: none;">
<iframe
id="scribeberry-widget"
allow="microphone"
></iframe>
</div>
<script>
let widgetLoaded = false;
async function toggleWidget() {
const container = document.getElementById('scribeberry-container');
const isHidden = container.style.display === 'none';
container.style.display = isHidden ? 'block' : 'none';
if (isHidden && !widgetLoaded) {
const response = await fetch('/api/scribeberry/token');
const { token } = await response.json();
document.getElementById('scribeberry-widget').src =
`https://app.scribeberry.com/widget?token=${token}`;
widgetLoaded = true;
}
}
</script>
Sending Patient Context
You can send patient information to Scribeberry so it appears in the user's session. This is optional but highly recommended.
Why Send Patient Context?
When you send patient medical context (allergies, medications, diagnoses), this data is automatically included in AI-generated notes. For example:
- • If you send
allergies: [{ name: 'Penicillin', severity: 'severe' }]
- • The generated SOAP note will include: "Allergies: Penicillin (severe)"
Without patient context, the AI can only work with what the provider says during the visit.
How It Works
Use the browser's postMessage API to communicate with the iframe:
const iframe = document.getElementById('scribeberry-widget');
iframe.contentWindow.postMessage(
{
type: 'PATIENT_CONTEXT',
patient: {
id: 'patient-123', // Required: Your patient ID
name: 'John Smith', // Required: Full name
firstName: 'John', // Optional: First name
lastName: 'Smith', // Optional: Last name
dob: '1985-03-15', // Optional: YYYY-MM-DD format
gender: 'male', // Optional: 'male' | 'female' | 'other' | 'unknown'
healthCardNumber: '123456789', // Optional: Health card number
email: 'john.smith@email.com', // Optional: Patient email
phone: '555-1234', // Optional: Patient phone
},
context: {
allergies: [
{
name: 'Penicillin', // Required: Allergy name
severity: 'severe', // Optional: 'mild' | 'moderate' | 'severe'
reaction: 'Rash' // Optional: Reaction description
}
],
medications: [
{
name: 'Metformin', // Required: Medication name
dosage: '500mg', // Optional: Dosage
frequency: 'twice daily', // Optional: Frequency
startDate: '2023-01-15', // Optional: Start date
status: 'active' // Optional: 'active' | 'discontinued'
}
],
diagnoses: [
{
name: 'Type 2 Diabetes', // Required: Diagnosis name
code: 'E11.9', // Optional: ICD-10, SNOMED, etc.
codeSystem: 'ICD-10', // Optional: Code system
date: '2023-01-01', // Optional: Diagnosis date
status: 'active' // Optional: 'active' | 'resolved'
}
],
labResults: [ // Optional: Lab results
{
name: 'Hemoglobin A1C',
value: '7.2',
unit: '%',
date: '2024-01-10',
normalRange: '4.0-5.6'
}
],
medicalHistory: 'Patient has history of hypertension and diabetes.', // Optional: Free text
chiefComplaint: 'Follow-up for diabetes management', // Optional: Reason for visit
customData: { // Optional: Any custom data
customField1: 'value1',
customField2: 'value2'
}
},
appointment: { // Optional: Appointment information
id: 'appt-456',
dateTime: '2024-01-16T10:00:00Z', // ISO 8601 format
type: 'Follow-up',
duration: 30, // Minutes
reason: 'Diabetes follow-up'
}
},
'https://app.scribeberry.com',
);
When to Send Patient Context
Send patient context when:
- User opens a patient chart in your EMR
- User switches to a different patient
- Patient data is updated
Available Patient Data Fields
You can send comprehensive patient information. Here are all available fields:
Patient Demographics
| Field |
Type |
Required |
Description |
Used in AI Notes |
patient.id |
string |
✓ Yes |
Your internal patient ID |
No |
patient.name |
string |
✓ Yes |
Full patient name |
✓ Yes |
patient.firstName |
string |
No |
First name |
✓ Yes |
patient.lastName |
string |
No |
Last name |
✓ Yes |
patient.dob |
string |
No |
Date of birth (YYYY-MM-DD) |
✓ Yes |
patient.gender |
string |
No |
'male' | 'female' | 'other' | 'unknown' |
✓ Yes |
patient.healthCardNumber |
string |
No |
Health card/insurance number |
No |
patient.email |
string |
No |
Patient email address |
No |
patient.phone |
string |
No |
Patient phone number |
No |
Medical Context
| Field |
Type |
Required |
Description |
Used in AI Notes |
context.allergies |
Array |
No |
Array of allergy objects with name, severity, reaction |
✓ Yes |
context.medications |
Array |
No |
Array of medication objects with name, dosage, frequency, status |
✓ Yes |
context.diagnoses |
Array |
No |
Array of diagnosis objects with name, code (ICD-10/SNOMED), status |
✓ Yes |
context.labResults |
Array |
No |
Array of lab result objects with name, value, unit, date |
✓ Yes |
context.medicalHistory |
string |
No |
Free text medical history |
✓ Yes |
context.chiefComplaint |
string |
No |
Reason for visit |
✓ Yes |
context.customData |
object |
No |
Any additional custom data |
No |
Appointment Information
| Field |
Type |
Required |
Description |
appointment.id |
string |
No |
Appointment ID |
appointment.dateTime |
string |
No |
ISO 8601 datetime |
appointment.type |
string |
No |
"Follow-up", "New Patient", etc. |
appointment.duration |
number |
No |
Duration in minutes |
appointment.reason |
string |
No |
Appointment reason |
Important: All medical context you send (allergies, medications, diagnoses, lab results, medical history, chief complaint) is automatically included in AI-generated notes. The more complete the data, the more accurate the generated notes.
Receiving Data from Scribeberry
When users click "Push to EMR" in Scribeberry, you receive the data via postMessage.
Basic Setup
window.addEventListener('message', (event) => {
// IMPORTANT: Verify the origin
if (event.origin !== 'https://app.scribeberry.com') {
return;
}
const data = event.data;
switch (data.type) {
case 'WIDGET_READY':
console.log('Scribeberry widget ready, version:', data.version);
break;
case 'PUSH_NOTE':
handlePushNote(data);
break;
case 'PUSH_DOCUMENT':
handlePushDocument(data);
break;
case 'SESSION_STARTED':
console.log('New session started:', data.sessionId);
break;
case 'TOKEN_EXPIRED':
// Session has ended, reload with new token
refreshToken();
break;
case 'TOKEN_EXPIRING':
// Session will expire soon - proactive warning
console.log('Token expiring in:', data.expiresInMs, 'ms');
break;
case 'REQUEST_TOKEN_REFRESH':
// Widget is requesting a new token
handleTokenRefreshRequest();
break;
case 'WIDGET_ERROR':
console.error('Widget error:', data.code, data.message);
break;
case 'REQUEST_PATIENT_CONTEXT':
// Widget is asking for patient data
sendPatientContext();
break;
}
});
Handling Push Note
function handlePushNote(data) {
console.log('Received note from Scribeberry:');
console.log('- Note:', data.note);
console.log('- Patient ID:', data.patientId);
console.log('- Session ID:', data.sessionId);
// Option 1: Put in a text field
document.getElementById('emr-notes-field').value = data.note;
// Option 2: Use sectional data if available
if (data.sections) {
document.getElementById('subjective-field').value = data.sections.subjective || '';
document.getElementById('objective-field').value = data.sections.objective || '';
document.getElementById('assessment-field').value = data.sections.assessment || '';
document.getElementById('plan-field').value = data.sections.plan || '';
}
// Option 3: Save via API
saveNoteToEMR(data.patientId, data.note, data.sections);
}
Complete Integration Example
Here's a complete example showing all message handlers:
<div id="scribeberry-container">
<iframe
id="scribeberry-widget"
style="width: 400px; height: 600px; border: none; border-radius: 12px;"
allow="microphone"
></iframe>
</div>
<script>
const SCRIBEBERRY_ORIGIN = 'https://app.scribeberry.com';
let currentPatient = null;
// Initialize widget
async function initScribeberry() {
const response = await fetch('/api/scribeberry/token');
const result = await response.json();
if (!result.success) {
console.error('Failed to get token:', result.error);
return;
}
document.getElementById('scribeberry-widget').src =
`${SCRIBEBERRY_ORIGIN}/widget?token=${result.data.token}`;
}
// Listen for messages from widget
window.addEventListener('message', async (event) => {
if (event.origin !== SCRIBEBERRY_ORIGIN) return;
const iframe = document.getElementById('scribeberry-widget');
const { type } = event.data;
switch (type) {
case 'WIDGET_READY':
console.log('Widget ready, version:', event.data.version);
// Send current patient if available
if (currentPatient) {
sendPatientContext(currentPatient);
}
break;
case 'REQUEST_PATIENT_CONTEXT':
// Widget is asking for patient data
if (currentPatient) {
sendPatientContext(currentPatient);
}
break;
case 'PUSH_NOTE':
handlePushNote(event.data);
break;
case 'PUSH_DOCUMENT':
handlePushDocument(event.data);
break;
case 'SESSION_STARTED':
console.log('Recording started, session:', event.data.sessionId);
break;
case 'REQUEST_TOKEN_REFRESH':
// Widget wants to extend session - get new token and send it
const refreshResponse = await fetch('/api/scribeberry/token');
const refreshResult = await refreshResponse.json();
if (refreshResult.success) {
iframe.contentWindow.postMessage(
{ type: 'TOKEN_REFRESH', token: refreshResult.data.token },
SCRIBEBERRY_ORIGIN
);
}
break;
case 'TOKEN_EXPIRED':
// Session ended - reload widget
initScribeberry();
break;
case 'WIDGET_ERROR':
console.error(`Widget error [${event.data.code}]:`, event.data.message);
break;
}
});
// Send patient context to widget
function sendPatientContext(patient) {
const iframe = document.getElementById('scribeberry-widget');
iframe.contentWindow.postMessage({
type: 'PATIENT_CONTEXT',
patient: {
id: patient.id,
name: patient.fullName,
dob: patient.dateOfBirth,
gender: patient.gender,
},
context: {
allergies: patient.allergies,
medications: patient.medications,
diagnoses: patient.diagnoses,
chiefComplaint: patient.visitReason,
}
}, SCRIBEBERRY_ORIGIN);
}
// Call when user opens a patient chart
function onPatientOpened(patient) {
currentPatient = patient;
sendPatientContext(patient);
}
// Handle received notes
function handlePushNote(data) {
// Insert into your EMR's note field or save via API
saveToEMR(data.patientId, {
note: data.note,
sections: data.sections,
transcript: data.transcript,
scribeberrySessionId: data.sessionId,
});
}
// Handle received documents
function handlePushDocument(data) {
saveDocumentToEMR(data.patientId, {
title: data.title,
content: data.content,
type: data.documentType,
pdfUrl: data.pdfUrl,
});
}
// Initialize on page load
initScribeberry();
</script>
API Reference
| Endpoint |
Method |
Auth |
Description |
/api/widget/token |
POST |
Partner Key |
Generate widget token |
POST /api/widget/token
Generate a JWT token for widget authentication.
Request:
POST https://app.scribeberry.com/api/widget/token
Content-Type: application/json
X-Partner-Key: your-partner-id.your-secret-key
{
"userEmail": "doctor@clinic.com",
"emrUserId": "user-123"
}
Success Response (200):
{
"success": true,
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresAt": "2024-01-16T12:00:00.000Z"
}
}
Error Response:
{
"success": false,
"error": "Invalid API key",
"code": "INVALID_API_KEY"
}
Data Schemas
Message Direction
- Inbound (EMR → Widget): PATIENT_CONTEXT, TOKEN_REFRESH, SET_CONTEXT, CLOSE_WIDGET
- Outbound (Widget → EMR): WIDGET_READY, PUSH_NOTE, PUSH_DOCUMENT, SESSION_STARTED, TOKEN_EXPIRED, TOKEN_EXPIRING, REQUEST_TOKEN_REFRESH, REQUEST_PATIENT_CONTEXT, WIDGET_ERROR
Inbound Messages (EMR → Widget)
PATIENT_CONTEXT
Send patient information when opening a chart or switching patients.
{
type: 'PATIENT_CONTEXT',
patient: {
id: string, // Required: Your patient ID
name: string, // Required: Full name
firstName?: string, // Optional: First name
lastName?: string, // Optional: Last name
dob?: string, // Optional: YYYY-MM-DD format
gender?: 'male' | 'female' | 'other' | 'unknown',
healthCardNumber?: string,
email?: string,
phone?: string,
},
context?: {
allergies?: Array<{
name: string,
severity?: 'mild' | 'moderate' | 'severe' | string,
reaction?: string,
}>,
medications?: Array<{
name: string,
dosage?: string,
frequency?: string,
startDate?: string,
status?: 'active' | 'discontinued' | string,
}>,
diagnoses?: Array<{
name: string,
code?: string, // ICD-10, SNOMED, etc.
codeSystem?: string,
date?: string,
status?: 'active' | 'resolved' | string,
}>,
labResults?: Array<{
name: string,
value?: string,
unit?: string,
date?: string,
normalRange?: string,
}>,
medicalHistory?: string, // Free text
chiefComplaint?: string, // Reason for visit
customData?: object, // Any additional data
},
appointment?: {
id?: string,
dateTime?: string, // ISO 8601
type?: string, // "Follow-up", "New Patient", etc.
duration?: number, // Minutes
reason?: string,
}
}
TOKEN_REFRESH
Send a new token in response to REQUEST_TOKEN_REFRESH to extend the session without reloading.
{
type: 'TOKEN_REFRESH',
token: string, // New JWT token
}
SET_CONTEXT
Send additional context that can be appended to notes.
{
type: 'SET_CONTEXT',
context: string, // Additional context text
mode?: 'append' | 'overwrite', // Default: 'append'
}
CLOSE_WIDGET
Request the widget to clean up and close.
{
type: 'CLOSE_WIDGET'
}
Outbound Messages (Widget → EMR)
WIDGET_READY
Sent when the widget is fully loaded and ready to receive messages.
{
type: 'WIDGET_READY',
version: string, // Widget version (e.g., "1.0.0")
}
PUSH_NOTE
Sent when user clicks "Push to EMR" for a clinical note.
{
type: 'PUSH_NOTE',
note: string, // Full note text
sections?: { // Optional sectional data
[sectionName: string]: string, // e.g., { subjective: "...", objective: "..." }
},
transcript?: string, // Optional visit transcript
sessionId: string, // Scribeberry session ID
patientId: string, // Patient this note is for
}
PUSH_DOCUMENT
Sent when user clicks "Push to EMR" for a document (letter, referral, form).
{
type: 'PUSH_DOCUMENT',
title: string, // Document title
content: string, // Document content (text or HTML)
documentType: 'letter' | 'referral' | 'form' | 'pdf',
pdfUrl?: string, // URL if PDF document
sessionId: string,
patientId: string,
}
SESSION_STARTED
Sent when user starts a new recording session.
{
type: 'SESSION_STARTED',
sessionId: string, // Unique session identifier
}
TOKEN_EXPIRING
Proactive warning that the session will expire soon.
{
type: 'TOKEN_EXPIRING',
expiresInMs: number, // Milliseconds until expiration
}
REQUEST_TOKEN_REFRESH
Widget requesting a new token to extend the session. Respond with TOKEN_REFRESH.
{
type: 'REQUEST_TOKEN_REFRESH'
}
TOKEN_EXPIRED
Session has ended. Widget must be reloaded with a new token.
{
type: 'TOKEN_EXPIRED'
}
REQUEST_PATIENT_CONTEXT
Widget requesting current patient data. Respond with PATIENT_CONTEXT.
{
type: 'REQUEST_PATIENT_CONTEXT'
}
WIDGET_ERROR
An error occurred in the widget.
{
type: 'WIDGET_ERROR',
code: string, // Error code (e.g., "NETWORK_ERROR")
message: string, // Human-readable error message
}
TypeScript Types
For TypeScript projects, here are the message type definitions:
// Inbound messages (EMR → Widget)
type WidgetInboundMessage =
| { type: 'PATIENT_CONTEXT'; patient: WidgetPatient; context?: WidgetMedicalContext; appointment?: WidgetAppointment }
| { type: 'TOKEN_REFRESH'; token: string }
| { type: 'SET_CONTEXT'; context: string; mode?: 'append' | 'overwrite' }
| { type: 'CLOSE_WIDGET' };
// Outbound messages (Widget → EMR)
type WidgetOutboundMessage =
| { type: 'WIDGET_READY'; version: string }
| { type: 'PUSH_NOTE'; note: string; sections?: Record<string, string>; transcript?: string; sessionId: string; patientId: string }
| { type: 'PUSH_DOCUMENT'; title: string; content: string; documentType: 'letter' | 'referral' | 'form' | 'pdf'; pdfUrl?: string; sessionId: string; patientId: string }
| { type: 'SESSION_STARTED'; sessionId: string }
| { type: 'TOKEN_EXPIRING'; expiresInMs: number }
| { type: 'REQUEST_TOKEN_REFRESH' }
| { type: 'TOKEN_EXPIRED' }
| { type: 'REQUEST_PATIENT_CONTEXT' }
| { type: 'WIDGET_ERROR'; code: string; message: string };
interface WidgetPatient {
id: string;
name: string;
firstName?: string;
lastName?: string;
dob?: string;
gender?: 'male' | 'female' | 'other' | 'unknown';
healthCardNumber?: string;
email?: string;
phone?: string;
}
interface WidgetMedicalContext {
allergies?: Array<{ name: string; severity?: string; reaction?: string }>;
medications?: Array<{ name: string; dosage?: string; frequency?: string; startDate?: string; status?: string }>;
diagnoses?: Array<{ name: string; code?: string; codeSystem?: string; date?: string; status?: string }>;
labResults?: Array<{ name: string; value?: string; unit?: string; date?: string; normalRange?: string }>;
medicalHistory?: string;
chiefComplaint?: string;
customData?: Record<string, unknown>;
}
interface WidgetAppointment {
id?: string;
dateTime?: string;
type?: string;
duration?: number;
reason?: string;
}
Error Handling
API Error Codes
All API errors include a code field for programmatic handling:
| Code |
Status |
Description |
Solution |
MISSING_API_KEY |
401 |
X-Partner-Key header not provided |
Add the header to your request |
INVALID_API_KEY |
401 |
API key not found or inactive |
Check your API key is correct |
INVALID_REQUEST |
400 |
Request body validation failed |
Check userEmail and emrUserId fields |
MISSING_TOKEN |
400 |
Token parameter not provided |
Include token in widget URL |
INVALID_TOKEN |
401 |
Token signature invalid |
Generate a new token |
TOKEN_EXPIRED |
401 |
Token has expired |
Generate a new token |
PARTNER_NOT_FOUND |
401 |
Partner account not found or inactive |
Contact Scribeberry support |
LINK_CONFLICT |
409 |
EMR user already linked to different Scribeberry account |
User must unlink existing account first |
SERVICE_UNAVAILABLE |
503 |
Widget service not configured |
Contact Scribeberry support |
INTERNAL_ERROR |
500 |
Server error |
Retry request or contact support |
Widget Errors
Listen for WIDGET_ERROR messages:
window.addEventListener('message', (event) => {
if (event.origin !== 'https://app.scribeberry.com') return;
if (event.data.type === 'WIDGET_ERROR') {
console.error(
`Scribeberry Error [${event.data.code}]: ${event.data.message}`
);
// Handle specific error codes
switch (event.data.code) {
case 'NETWORK_ERROR':
showNotification('Network error. Please check your connection.');
break;
case 'MICROPHONE_DENIED':
showNotification('Microphone access required for recording.');
break;
default:
showNotification('An error occurred. Please try again.');
}
}
});
Token Expiration
Scribeberry sends proactive warnings before tokens expire, allowing seamless session extension:
window.addEventListener('message', async (event) => {
if (event.origin !== 'https://app.scribeberry.com') return;
const iframe = document.getElementById('scribeberry-widget');
switch (event.data.type) {
case 'TOKEN_EXPIRING':
// Proactive warning - session will expire soon
// Good time to prepare a new token
console.log('Token expiring in:', event.data.expiresInMs, 'ms');
break;
case 'REQUEST_TOKEN_REFRESH':
// Widget is requesting a session extension
// Generate new token and send it back
const response = await fetch('/api/scribeberry/token');
const { data } = await response.json();
iframe.contentWindow.postMessage(
{ type: 'TOKEN_REFRESH', token: data.token },
'https://app.scribeberry.com'
);
break;
case 'TOKEN_EXPIRED':
// Session has ended - must reload widget
const newTokenResponse = await fetch('/api/scribeberry/token');
const { data: newData } = await newTokenResponse.json();
iframe.src = `https://app.scribeberry.com/widget?token=${newData.token}`;
break;
}
});
Security
Allowed Origins Configuration
When you register as a widget partner, Scribeberry configures your allowed origins. These are used for:
- postMessage Security: Scribeberry will only send messages to your registered origins
- Origin Validation: Scribeberry will only accept messages from your registered origins
- CORS Headers: API responses include appropriate CORS headers for your origins
Best Practices
1. Never expose your API key in frontend code
// ❌ BAD - API key in frontend
fetch('https://app.scribeberry.com/api/widget/token', {
headers: { 'X-Partner-Key': 'your-secret-key' },
});
// ✅ GOOD - Call your backend instead
fetch('/api/scribeberry/token');
2. Always verify message origins
window.addEventListener('message', (event) => {
// ✅ GOOD - Check origin
if (event.origin !== 'https://app.scribeberry.com') return;
// Process message...
});
3. Use HTTPS everywhere
- • Your EMR must use HTTPS
- • All API calls are HTTPS only
FAQ
General
Does this work with any EMR?
Yes! Any web-based EMR can embed the widget using an iframe.
What if our EMR uses React/Angular/Vue?
The iframe approach works with any framework. See framework-specific examples in the Quick Start section.
Do users need a Scribeberry account?
Yes. First-time users will create/link their account within the widget.
How does billing work?
Users need a Scribeberry subscription. This is handled within the widget.
Technical
Can we customize the widget appearance?
Iframe Container: Yes! You can fully style the iframe container (size, position, borders, shadows, responsive design). See the "Embedding the Widget" section for examples.
Widget Content: The internal widget UI has a fixed appearance (colors, fonts, buttons). Custom theming for widget content is on our roadmap.
What browsers are supported?
All modern browsers: Chrome 80+, Firefox 75+, Safari 13+, Edge 80+
Is there a sandbox/test environment?
Contact us for sandbox credentials. We'll set up your staging domain in our allowed origins.
Can we track usage/analytics?
You can track widget opens and push events. Detailed analytics coming soon.
What data do you need to set us up?
Your company name, all domains where the widget will be embedded (including staging), and a technical contact email.
How long does setup take?
We can generate your API credentials same-day. Integration typically takes 15-30 minutes.
What happens if a token expires?
The widget sends proactive warnings:
- • TOKEN_EXPIRING: Sent before expiration with time remaining
- • REQUEST_TOKEN_REFRESH: Widget requests a new token - respond with TOKEN_REFRESH to extend session without reload
- • TOKEN_EXPIRED: Session ended - must reload widget with new token
Can we send patient data after the widget loads?
Yes! You can send patient context at any time using postMessage. It's recommended to send it when a user opens a patient chart or switches patients.
What if patient context doesn't appear in generated notes?
Make sure you're sending the data in the correct format (see Data Schemas section). The most common issue is missing the 'name' field in allergies/medications/diagnoses arrays. Also ensure you send context before the user starts recording.
Support
What We Need From You
When onboarding, please provide:
| Item |
Description |
Example |
| Company Name |
Your EMR's display name |
"ACME EMR" |
| Domains |
All domains where widget will be embedded |
https://app.acme-emr.com |
| Technical Contact |
Email for integration support |
tech@your-emr.com |
| Expected Users |
Rough estimate |
~500 users |
Ready to Integrate?
Get your API key and start integrating today
CONTACT PARTNERSHIPS