Widget API Documentation

Complete integration guide for embedding Scribeberry into your EMR

Integration time: ~15-30 minutes

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. 1. Your backend requests a token from Scribeberry (one API call with your partner key)
  2. 2. Your frontend embeds the widget using that token (simple iframe)
  3. 3. Optionally send patient context to pre-fill data (postMessage API)
  4. 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:

  1. User opens a patient chart in your EMR
  2. User switches to a different patient
  3. 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:

  1. postMessage Security: Scribeberry will only send messages to your registered origins
  2. Origin Validation: Scribeberry will only accept messages from your registered origins
  3. 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

Contact

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