Lesson 7.3 — AI Gridhooks: How to integrate your own Apps with DeepCura

Module 7, Lesson 3

Prerequisites: Lesson 7.2 — Workflow: Clinic vs Hospital

Estimated time: 10 minutes

What You'll Learn

  • What AI Gridhooks are and how they work
  • Why Gridhooks are beneficial for both doctors and developers
  • How to integrate Gridhooks easily (including a Google Spreadsheet boilerplate script)
  • Code snippets that show how this system works on the frontend and backend

By the end of this read, you'll have a clear picture of how AI Gridhooks can empower your practice or your next big software project.

What Are AI Gridhooks?

AI Gridhooks are an evolution of the standard webhook. Instead of having a single linear pipeline—imagine a one-dimensional line—they use a grid-like architecture.

In this grid structure, multiple AI or natural language calls can run in parallel or in a sequence that you define. Each call can extract, transform, or create new pieces of data from the same source text (like a clinical note or a transcript), resulting in multiple outputs instead of just one.

Why Should Doctors Care?

Own Your Data: AI Gridhooks let you, as a clinician, define how your data is transformed or organized. That means if you have particular fields to fill out — like CPT codes, insurance details, or summaries — you can do it automatically with minimal manual effort.

Instant Documentation: Streamline clinical documentation by feeding patient transcripts or clinical notes into the AI Gridhook and instantly generating multiple structured elements. This can be extremely handy for progress notes, billing forms, or even specialized data needed for research.

HIPAA Compliance: All data is processed in an encrypted manner, ensuring patient confidentiality and meeting compliance standards. You have complete control over where your data is sent and how it's protected.

Easy Integration (Google Spreadsheets): Many clinics use spreadsheets for a quick overview of patient data or internal processes. With AI Gridhooks, you can push newly generated JSON fields right into a Google Spreadsheet. We provide a boilerplate Google Apps Script so you can start in minutes!

Why Should Developers Care?

Flexible Architecture: No longer limited to a single response, you can orchestrate multiple transformations or extractions in one go. For developers building dashboards, AI-driven applications, or advanced analytics tools, this significantly cuts down on the number of separate processes or calls needed.

Dynamic JSON Schemas: By composing the schema on the fly, you can adapt your integration to each use case. This is particularly helpful when different clients or teams require different data structures without rewriting large chunks of code.

Reduced Overhead: Because multiple AI calls are consolidated into one pipeline, you spend fewer resources (both computational and time) on separate transformations.

Plug-and-Play: Easily integrate the DeepCura platform with your own application or other third-party systems. The data is output in standard JSON, so hooking it into existing microservices, BI tools, or ML pipelines is straightforward.

How It Works: Designing Your Schema with Natural Language

On the frontend, you have a clean component that allows you to define the fields you want in your JSON structure. Each "layer" or "grid" has six fields, which you can name however you like:

gridhook configuration interface

  • You can label these fields in plain English (or any language).
  • The instructions for how to populate those fields go in the value.
  • Once you save these settings, the backend uses them to build the prompts for the AI model. Essentially, you're designing your JSON schema with natural language instructions.

Potential Use Cases

Clinical Documentation & Billing: Doctors can structure their notes for direct import into billing systems or insurance forms. The automated extraction of diagnoses, CPT codes, and patient details saves time and reduces errors.

Research & Analytics: Multiple JSON outputs can capture data relevant for research, like specific terms in patient narratives or aggregated outcomes. This can feed directly into dashboards or analytics software.

Quality Assurance: Use AI Gridhooks to spot inconsistencies in clinical documentation by extracting certain relevant fields and comparing them across multiple notes.

Cross-System Integrations: You can integrate these structured outputs with EHR systems, third-party apps, or machine learning pipelines. Having a structured JSON schema on-the-fly eliminates a lot of repetitive work.

Common Gridhook Use Cases

  • Populate a Google Sheet with patient demographics, diagnoses, and treatment plans from each encounter.
  • Send data to a practice management system that does not have a direct EHR integration.
  • Feed data into analytics tools for custom reporting beyond what the built-in analytics dashboard provides.
  • Trigger workflows in automation platforms like Zapier or Make by sending webhooks.

How Gridhooks Work (Setup)

  1. Enable Gridhooks — Toggle the "AI Gridhooks" setting in the Automation Settings page.
  2. Configure destinations — Set up where extracted data should be sent (Google Spreadsheets, custom webhooks, or third-party systems).
  3. Define extraction rules — Specify what data points the AI should extract from your recordings (diagnoses, medications, vitals, etc.).
  4. Automatic delivery — After each recording, the AI extracts the configured data points and sends them to your destinations automatically.

Integration With Google Spreadsheets (Boilerplate) - Video

One of the most exciting parts is how easily you can integrate your brand-new AI Gridhook data into a Google Spreadsheet. Here's a high-level look at how you might set up a Google Apps Script to receive and populate your sheet:

Follow the YouTube tutorial below to understand how you can integrate with Google Spreadsheets:

Here is the single file you will need to copy and paste:

  • 
    // This function runs when the spreadsheet is opened and adds a custom menu.
    function onOpen() {
      SpreadsheetApp.getUi()
          .createMenu('DeepCura AI')
          .addItem('Configure Script', 'showConfigurationDialog')
          .addToUi();
    }
    
    // This function shows a dialog to configure the script.
    function showConfigurationDialog() {
      var ui = SpreadsheetApp.getUi();
      
      // Automatically get and store the current Spreadsheet ID
      var sheetId = SpreadsheetApp.getActiveSpreadsheet().getId();
      PropertiesService.getScriptProperties().setProperty('GOOGLE_SHEET_ID', sheetId);
      
      // Prompt for Secret Key
      var result = ui.prompt(
          'Script Configuration',
          'Please enter your Secret Key (GOOGLE_SHEET_ID: ' + sheetId + ' has been automatically set):',
          ui.ButtonSet.OK_CANCEL);
          
      if (result.getSelectedButton() == ui.Button.OK) {
        var secretKey = result.getResponseText();
        if (secretKey && secretKey.trim() !== "") {
          PropertiesService.getScriptProperties().setProperty('SECRET_KEY', secretKey);
          ui.alert('Configuration Saved', 'GOOGLE_SHEET_ID and SECRET_KEY have been saved. Your Web App can now process requests.', ui.ButtonSet.OK);
        } else {
          ui.alert('Error', 'Secret Key cannot be empty. Configuration was not fully saved.', ui.ButtonSet.OK);
        }
      } else {
        ui.alert('Configuration Cancelled', 'SECRET_KEY was not set. Configuration might be incomplete.', ui.ButtonSet.OK);
      }
    }
    
    // Decryption function (remains the same)
    function decryptWithAES(encryptedText, secretKey) {
      try {
        const [ivBase64, encryptedBase64] = encryptedText.split(':');
        const iv = CryptoJS.enc.Base64.parse(ivBase64);
        const encryptedData = CryptoJS.enc.Base64.parse(encryptedBase64);
        const paddedKey = CryptoJS.enc.Utf8.parse(secretKey.padEnd(32, '\0').slice(0, 32));
        
        const decrypted = CryptoJS.AES.decrypt(
          { ciphertext: encryptedData },
          paddedKey,
          { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }
        );
        
        return decrypted.toString(CryptoJS.enc.Utf8);
      } catch (e) {
        console.error("AES decryption error: " + e.message);
        return null;
      }
    }
    
    // This function will handle the incoming POST request
    function doPost(e) {
      const scriptProperties = PropertiesService.getScriptProperties();
      const GOOGLE_SHEET_ID = scriptProperties.getProperty('GOOGLE_SHEET_ID');
      const SECRET_KEY = scriptProperties.getProperty('SECRET_KEY');
    
      // Check if configuration is complete
      if (!GOOGLE_SHEET_ID || !SECRET_KEY) {
        return ContentService.createTextOutput(
          JSON.stringify({ 
            status: "error", 
            message: "Configuration incomplete. Please open the Google Sheet and run 'Configure Script' from the 'My Custom Menu'." 
          })
        ).setMimeType(ContentService.MimeType.JSON);
      }
    
      var requestData;
      try {
        requestData = JSON.parse(e.postData.contents);
      } catch (parseError) {
        return ContentService.createTextOutput(
          JSON.stringify({ status: "error", message: "Invalid JSON format" })
        ).setMimeType(ContentService.MimeType.JSON);
      }
    
      // Decrypt the main data field and parse its JSON
      var decryptedText = decryptWithAES(requestData.data, SECRET_KEY);
      if (!decryptedText) {
        return ContentService.createTextOutput(
          JSON.stringify({ status: "error", message: "Decryption failed for data. Check Secret Key or data format." })
        ).setMimeType(ContentService.MimeType.JSON);
      }
      
      var jsonData;
      try {
        jsonData = JSON.parse(decryptedText);
      } catch (jsonError) {
        return ContentService.createTextOutput(
          JSON.stringify({ status: "error", message: "Invalid decrypted JSON" })
        ).setMimeType(ContentService.MimeType.JSON);
      }
      
      // Decrypt "Source Transcript" and "Source AI Note"
      var decryptedSourceTranscript = decryptWithAES(requestData.sourceTranscript, SECRET_KEY);
      if (!decryptedSourceTranscript) {
        return ContentService.createTextOutput(
          JSON.stringify({ status: "error", message: "Decryption failed for Source Transcript" })
        ).setMimeType(ContentService.MimeType.JSON);
      }
      
      var decryptedSourceAINote = decryptWithAES(requestData.sourceAINote, SECRET_KEY);
      if (!decryptedSourceAINote) {
        return ContentService.createTextOutput(
          JSON.stringify({ status: "error", message: "Decryption failed for Source AI Note" })
        ).setMimeType(ContentService.MimeType.JSON);
      }
    
      try {
        // Prepare sheet data with the additional Base64 column
        var sheet = SpreadsheetApp.openById(GOOGLE_SHEET_ID).getSheetByName('Sheet1'); // Or your desired sheet name
        if (!sheet) {
           // Attempt to create the sheet if it doesn't exist
          sheet = SpreadsheetApp.openById(GOOGLE_SHEET_ID).insertSheet('Sheet1');
          if (!sheet) {
             return ContentService.createTextOutput(
              JSON.stringify({ status: "error", message: "Sheet 'Sheet1' could not be found or created in the configured Google Sheet." })
            ).setMimeType(ContentService.MimeType.JSON);
          }
        }
    
        var keyRow = ["Provider ID","Provider Name", "Note Id", "Source Transcript", "Source AI Note", "Template", "PDFBase64"];
        var row = [
          requestData.providerId,
          requestData.providerName,
          requestData.noteId,
          decryptedSourceTranscript,  // Use decrypted value
          decryptedSourceAINote,      // Use decrypted value
          requestData.template,
          requestData.base64         // Append the base64 value here
        ];
    
        // Add dynamic keys from the decrypted JSON data
        for (var key in jsonData) {
          if (jsonData.hasOwnProperty(key)) {
            keyRow.push(key);
            row.push(jsonData[key]);
          }
        }
    
        // Write headers if the sheet is empty or if headers don't match (optional enhancement)
        if (sheet.getLastRow() === 0) {
          sheet.appendRow(keyRow);
        }
        // Append the data row
        sheet.appendRow(row);
    
        return ContentService.createTextOutput(
          JSON.stringify({ status: "success", message: "Data saved successfully" })
        ).setMimeType(ContentService.MimeType.JSON);
    
      } catch (sheetError) {
        console.error("Google Sheet operation error: " + sheetError.message);
        Logger.log("Error during sheet operation: " + sheetError.toString());
        return ContentService.createTextOutput(
          JSON.stringify({ status: "error", message: "Error interacting with Google Sheet: " + sheetError.message })
        ).setMimeType(ContentService.MimeType.JSON);
      }
    }
    
    
    // --- CryptoJS Core Components (core.js) ---
    ;(function (root, factory) {
      if (typeof exports === "object") {
        // CommonJS
        module.exports = exports = factory();
      }
      else if (typeof define === "function" && define.amd) {
        // AMD
        define([], factory);
      }
      else {
        // Global (browser)
        root.CryptoJS = factory();
      }
    }(this, function () {
      var CryptoJS = CryptoJS || (function (Math, undefined) {
          var crypto;
          if (typeof window !== 'undefined' && window.crypto) {
              crypto = window.crypto;
          }
          if (typeof self !== 'undefined' && self.crypto) {
              crypto = self.crypto;
          }
          if (typeof globalThis !== 'undefined' && globalThis.crypto) {
              crypto = globalThis.crypto;
          }
          if (!crypto && typeof window !== 'undefined' && window.msCrypto) {
              crypto = window.msCrypto;
          }
          if (!crypto && typeof global !== 'undefined' && global.crypto) {
              crypto = global.crypto;
          }
          if (!crypto && typeof require === 'function') {
              try {
                  crypto = require('crypto');
              } catch (err) {}
          }
          var cryptoSecureRandomInt = function () {
              if (crypto) {
                  if (typeof crypto.getRandomValues === 'function') {
                      try {
                          return crypto.getRandomValues(new Uint32Array(1))[0];
                      } catch (err) {}
                  }
                  if (typeof crypto.randomBytes === 'function') {
                      try {
                          return crypto.randomBytes(4).readInt32LE();
                      } catch (err) {}
                  }
              }
              throw new Error('Native crypto module could not be used to get secure random number.');
          };
          var create = Object.create || (function () {
              function F() {}
              return function (obj) {
                  var subtype;
                  F.prototype = obj;
                  subtype = new F();
                  F.prototype = null;
                  return subtype;
              };
          }());
          var C = {};
          var C_lib = C.lib = {};
          var Base = C_lib.Base = (function () {
              return {
                  extend: function (overrides) {
                      var subtype = create(this);
                      if (overrides) {
                          subtype.mixIn(overrides);
                      }
                      if (!subtype.hasOwnProperty('init') || this.init === subtype.init) {
                          subtype.init = function () {
                              subtype.$super.init.apply(this, arguments);
                          };
                      }
                      subtype.init.prototype = subtype;
                      subtype.$super = this;
                      return subtype;
                  },
                  create: function () {
                      var instance = this.extend();
                      instance.init.apply(instance, arguments);
                      return instance;
                  },
                  init: function () {},
                  mixIn: function (properties) {
                      for (var propertyName in properties) {
                          if (properties.hasOwnProperty(propertyName)) {
                              this[propertyName] = properties[propertyName];
                          }
                      }
                      if (properties.hasOwnProperty('toString')) {
                          this.toString = properties.toString;
                      }
                  },
                  clone: function () {
                      return this.init.prototype.extend(this);
                  }
              };
          }());
          var WordArray = C_lib.WordArray = Base.extend({
              init: function (words, sigBytes) {
                  words = this.words = words || [];
                  if (sigBytes != undefined) {
                      this.sigBytes = sigBytes;
                  } else {
                      this.sigBytes = words.length * 4;
                  }
              },
              toString: function (encoder) {
                  return (encoder || Hex).stringify(this);
              },
              concat: function (wordArray) {
                  var thisWords = this.words;
                  var thatWords = wordArray.words;
                  var thisSigBytes = this.sigBytes;
                  var thatSigBytes = wordArray.sigBytes;
                  this.clamp();
                  if (thisSigBytes % 4) {
                      for (var i = 0; i < thatSigBytes; i++) {
                          var thatByte = (thatWords[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
                          thisWords[(thisSigBytes + i) >>> 2] |= thatByte << (24 - ((thisSigBytes + i) % 4) * 8);
                      }
                  } else {
                      for (var j = 0; j < thatSigBytes; j += 4) {
                          thisWords[(thisSigBytes + j) >>> 2] = thatWords[j >>> 2];
                      }
                  }
                  this.sigBytes += thatSigBytes;
                  return this;
              },
              clamp: function () {
                  var words = this.words;
                  var sigBytes = this.sigBytes;
                  words[sigBytes >>> 2] &= 0xffffffff << (32 - (sigBytes % 4) * 8);
                  words.length = Math.ceil(sigBytes / 4);
              },
              clone: function () {
                  var clone = Base.clone.call(this);
                  clone.words = this.words.slice(0);
                  return clone;
              },
              random: function (nBytes) {
                  var words = [];
                  for (var i = 0; i < nBytes; i += 4) {
                      words.push(cryptoSecureRandomInt());
                  }
                  return new WordArray.init(words, nBytes);
              }
          });
          var C_enc = C.enc = {};
          var Hex = C_enc.Hex = {
              stringify: function (wordArray) {
                  var words = wordArray.words;
                  var sigBytes = wordArray.sigBytes;
                  var hexChars = [];
                  for (var i = 0; i < sigBytes; i++) {
                      var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
                      hexChars.push((bite >>> 4).toString(16));
                      hexChars.push((bite & 0x0f).toString(16));
                  }
                  return hexChars.join('');
              },
              parse: function (hexStr) {
                  var hexStrLength = hexStr.length;
                  var words = [];
                  for (var i = 0; i < hexStrLength; i += 2) {
                      words[i >>> 3] |= parseInt(hexStr.substr(i, 2), 16) << (24 - (i % 8) * 4);
                  }
                  return new WordArray.init(words, hexStrLength / 2);
              }
          };
          var Latin1 = C_enc.Latin1 = {
              stringify: function (wordArray) {
                  var words = wordArray.words;
                  var sigBytes = wordArray.sigBytes;
                  var latin1Chars = [];
                  for (var i = 0; i < sigBytes; i++) {
                      var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
                      latin1Chars.push(String.fromCharCode(bite));
                  }
                  return latin1Chars.join('');
              },
              parse: function (latin1Str) {
                  var latin1StrLength = latin1Str.length;
                  var words = [];
                  for (var i = 0; i < latin1StrLength; i++) {
                      words[i >>> 2] |= (latin1Str.charCodeAt(i) & 0xff) << (24 - (i % 4) * 8);
                  }
                  return new WordArray.init(words, latin1StrLength);
              }
          };
          var Utf8 = C_enc.Utf8 = {
              stringify: function (wordArray) {
                  try {
                      return decodeURIComponent(escape(Latin1.stringify(wordArray)));
                  } catch (e) {
                      throw new Error('Malformed UTF-8 data');
                  }
              },
              parse: function (utf8Str) {
                  return Latin1.parse(unescape(encodeURIComponent(utf8Str)));
              }
          };
          var BufferedBlockAlgorithm = C_lib.BufferedBlockAlgorithm = Base.extend({
              reset: function () {
                  this._data = new WordArray.init();
                  this._nDataBytes = 0;
              },
              _append: function (data) {
                  if (typeof data == 'string') {
                      data = Utf8.parse(data);
                  }
                  this._data.concat(data);
                  this._nDataBytes += data.sigBytes;
              },
              _process: function (doFlush) {
                  var processedWords;
                  var data = this._data;
                  var dataWords = data.words;
                  var dataSigBytes = data.sigBytes;
                  var blockSize = this.blockSize;
                  var blockSizeBytes = blockSize * 4;
                  var nBlocksReady = dataSigBytes / blockSizeBytes;
                  if (doFlush) {
                      nBlocksReady = Math.ceil(nBlocksReady);
                  } else {
                      nBlocksReady = Math.max((nBlocksReady | 0) - this._minBufferSize, 0);
                  }
                  var nWordsReady = nBlocksReady * blockSize;
                  var nBytesReady = Math.min(nWordsReady * 4, dataSigBytes);
                  if (nWordsReady) {
                      for (var offset = 0; offset < nWordsReady; offset += blockSize) {
                          this._doProcessBlock(dataWords, offset);
                      }
                      processedWords = dataWords.splice(0, nWordsReady);
                      data.sigBytes -= nBytesReady;
                  }
                  return new WordArray.init(processedWords, nBytesReady);
              },
              clone: function () {
                  var clone = Base.clone.call(this);
                  clone._data = this._data.clone();
                  return clone;
              },
              _minBufferSize: 0
          });
          var Hasher = C_lib.Hasher = BufferedBlockAlgorithm.extend({
              cfg: Base.extend(),
              init: function (cfg) {
                  this.cfg = this.cfg.extend(cfg);
                  this.reset();
              },
              reset: function () {
                  BufferedBlockAlgorithm.reset.call(this);
                  this._doReset();
              },
              update: function (messageUpdate) {
                  this._append(messageUpdate);
                  this._process();
                  return this;
              },
              finalize: function (messageUpdate) {
                  if (messageUpdate) {
                      this._append(messageUpdate);
                  }
                  var hash = this._doFinalize();
                  return hash;
              },
              blockSize: 512/32,
              _createHelper: function (hasher) {
                  return function (message, cfg) {
                      return new hasher.init(cfg).finalize(message);
                  };
              },
              _createHmacHelper: function (hasher) {
                  return function (message, key) {
                      return new C_algo.HMAC.init(hasher, key).finalize(message);
                  };
              }
          });
          var C_algo = C.algo = {};
          return C;
      }(Math));
      return CryptoJS;
    }));
    
    // --- CryptoJS Base64 Encoding (enc-base64.js) ---
    ;(function (root, factory) {
      if (typeof exports === "object") {
        module.exports = exports = factory(root.CryptoJS || require("./core")); // Adapt if core is not global yet
      }
      else if (typeof define === "function" && define.amd) {
        define(["./core"], factory);
      }
      else {
        factory(root.CryptoJS);
      }
    }(this, function (CryptoJS) {
      (function () {
          var C = CryptoJS;
          var C_lib = C.lib;
          var WordArray = C_lib.WordArray;
          var C_enc = C.enc;
          var Base64 = C_enc.Base64 = {
              stringify: function (wordArray) {
                  var words = wordArray.words;
                  var sigBytes = wordArray.sigBytes;
                  var map = this._map;
                  wordArray.clamp();
                  var base64Chars = [];
                  for (var i = 0; i < sigBytes; i += 3) {
                      var byte1 = (words[i >>> 2]       >>> (24 - (i % 4) * 8))       & 0xff;
                      var byte2 = (words[(i + 1) >>> 2] >>> (24 - ((i + 1) % 4) * 8)) & 0xff;
                      var byte3 = (words[(i + 2) >>> 2] >>> (24 - ((i + 2) % 4) * 8)) & 0xff;
                      var triplet = (byte1 << 16) | (byte2 << 8) | byte3;
                      for (var j = 0; (j < 4) && (i + j * 0.75 < sigBytes); j++) {
                          base64Chars.push(map.charAt((triplet >>> (6 * (3 - j))) & 0x3f));
                      }
                  }
                  var paddingChar = map.charAt(64);
                  if (paddingChar) {
                      while (base64Chars.length % 4) {
                          base64Chars.push(paddingChar);
                      }
                  }
                  return base64Chars.join('');
              },
              parse: function (base64Str) {
                  var base64StrLength = base64Str.length;
                  var map = this._map;
                  var reverseMap = this._reverseMap;
                  if (!reverseMap) {
                          reverseMap = this._reverseMap = [];
                          for (var j = 0; j < map.length; j++) {
                              reverseMap[map.charCodeAt(j)] = j;
                          }
                  }
                  var paddingChar = map.charAt(64);
                  if (paddingChar) {
                      var paddingIndex = base64Str.indexOf(paddingChar);
                      if (paddingIndex !== -1) {
                          base64StrLength = paddingIndex;
                      }
                  }
                  return parseLoop(base64Str, base64StrLength, reverseMap);
              },
              _map: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
          };
          function parseLoop(base64Str, base64StrLength, reverseMap) {
            var words = [];
            var nBytes = 0;
            for (var i = 0; i < base64StrLength; i++) {
                if (i % 4) {
                    var bits1 = reverseMap[base64Str.charCodeAt(i - 1)] << ((i % 4) * 2);
                    var bits2 = reverseMap[base64Str.charCodeAt(i)] >>> (6 - (i % 4) * 2);
                    var bitsCombined = bits1 | bits2;
                    words[nBytes >>> 2] |= bitsCombined << (24 - (nBytes % 4) * 8);
                    nBytes++;
                }
            }
            return WordArray.create(words, nBytes);
          }
      }());
      return CryptoJS.enc.Base64;
    }));
    
    // --- CryptoJS Cipher Core Components (cipher-core.js) ---
    ;(function (root, factory, undef) {
      if (typeof exports === "object") {
        module.exports = exports = factory(root.CryptoJS || require("./core"), require("./evpkdf"));
      }
      else if (typeof define === "function" && define.amd) {
        define(["./core", "./evpkdf"], factory);
      }
      else {
        factory(root.CryptoJS);
      }
    }(this, function (CryptoJS) {
      CryptoJS.lib.Cipher || (function (undefined) {
          var C = CryptoJS;
          var C_lib = C.lib;
          var Base = C_lib.Base;
          var WordArray = C_lib.WordArray;
          var BufferedBlockAlgorithm = C_lib.BufferedBlockAlgorithm;
          var C_enc = C.enc;
          var Utf8 = C_enc.Utf8;
          var Base64 = C_enc.Base64;
          var C_algo = C.algo;
          var EvpKDF = C_algo.EvpKDF; // This is included by cipher-core, but not strictly used by OP's decrypt
          var Cipher = C_lib.Cipher = BufferedBlockAlgorithm.extend({
              cfg: Base.extend(),
              createEncryptor: function (key, cfg) {
                  return this.create(this._ENC_XFORM_MODE, key, cfg);
              },
              createDecryptor: function (key, cfg) {
                  return this.create(this._DEC_XFORM_MODE, key, cfg);
              },
              init: function (xformMode, key, cfg) {
                  this.cfg = this.cfg.extend(cfg);
                  this._xformMode = xformMode;
                  this._key = key;
                  this.reset();
              },
              reset: function () {
                  BufferedBlockAlgorithm.reset.call(this);
                  this._doReset();
              },
              process: function (dataUpdate) {
                  this._append(dataUpdate);
                  return this._process();
              },
              finalize: function (dataUpdate) {
                  if (dataUpdate) {
                      this._append(dataUpdate);
                  }
                  var finalProcessedData = this._doFinalize();
                  return finalProcessedData;
              },
              keySize: 128/32,
              ivSize: 128/32,
              _ENC_XFORM_MODE: 1,
              _DEC_XFORM_MODE: 2,
              _createHelper: (function () {
                  function selectCipherStrategy(key) {
                      if (typeof key == 'string') {
                          return PasswordBasedCipher;
                      } else {
                          return SerializableCipher;
                      }
                  }
                  return function (cipher) {
                      return {
                          encrypt: function (message, key, cfg) {
                              return selectCipherStrategy(key).encrypt(cipher, message, key, cfg);
                          },
                          decrypt: function (ciphertext, key, cfg) {
                              return selectCipherStrategy(key).decrypt(cipher, ciphertext, key, cfg);
                          }
                      };
                  };
              }())
          });
          var StreamCipher = C_lib.StreamCipher = Cipher.extend({
              _doFinalize: function () {
                  var finalProcessedBlocks = this._process(!!'flush');
                  return finalProcessedBlocks;
              },
              blockSize: 1
          });
          var C_mode = C.mode = {};
          var BlockCipherMode = C_lib.BlockCipherMode = Base.extend({
              createEncryptor: function (cipher, iv) {
                  return this.Encryptor.create(cipher, iv);
              },
              createDecryptor: function (cipher, iv) {
                  return this.Decryptor.create(cipher, iv);
              },
              init: function (cipher, iv) {
                  this._cipher = cipher;
                  this._iv = iv;
              }
          });
          var CBC = C_mode.CBC = (function () {
              var CBC = BlockCipherMode.extend();
              CBC.Encryptor = CBC.extend({
                  processBlock: function (words, offset) {
                      var cipher = this._cipher;
                      var blockSize = cipher.blockSize;
                      xorBlock.call(this, words, offset, blockSize);
                      cipher.encryptBlock(words, offset);
                      this._prevBlock = words.slice(offset, offset + blockSize);
                  }
              });
              CBC.Decryptor = CBC.extend({
                  processBlock: function (words, offset) {
                      var cipher = this._cipher;
                      var blockSize = cipher.blockSize;
                      var thisBlock = words.slice(offset, offset + blockSize);
                      cipher.decryptBlock(words, offset);
                      xorBlock.call(this, words, offset, blockSize);
                      this._prevBlock = thisBlock;
                  }
              });
              function xorBlock(words, offset, blockSize) {
                  var block;
                  var iv = this._iv;
                  if (iv) {
                      block = iv;
                      this._iv = undefined;
                  } else {
                      block = this._prevBlock;
                  }
                  for (var i = 0; i < blockSize; i++) {
                      words[offset + i] ^= block[i];
                  }
              }
              return CBC;
          }());
          var C_pad = C.pad = {};
          var Pkcs7 = C_pad.Pkcs7 = {
              pad: function (data, blockSize) {
                  var blockSizeBytes = blockSize * 4;
                  var nPaddingBytes = blockSizeBytes - data.sigBytes % blockSizeBytes;
                  var paddingWord = (nPaddingBytes << 24) | (nPaddingBytes << 16) | (nPaddingBytes << 8) | nPaddingBytes;
                  var paddingWords = [];
                  for (var i = 0; i < nPaddingBytes; i += 4) {
                      paddingWords.push(paddingWord);
                  }
                  var padding = WordArray.create(paddingWords, nPaddingBytes);
                  data.concat(padding);
              },
              unpad: function (data) {
                  var nPaddingBytes = data.words[(data.sigBytes - 1) >>> 2] & 0xff;
                  data.sigBytes -= nPaddingBytes;
              }
          };
          var BlockCipher = C_lib.BlockCipher = Cipher.extend({
              cfg: Cipher.cfg.extend({
                  mode: CBC,
                  padding: Pkcs7
              }),
              reset: function () {
                  var modeCreator;
                  Cipher.reset.call(this);
                  var cfg = this.cfg;
                  var iv = cfg.iv;
                  var mode = cfg.mode;
                  if (this._xformMode == this._ENC_XFORM_MODE) {
                      modeCreator = mode.createEncryptor;
                  } else {
                      modeCreator = mode.createDecryptor;
                      this._minBufferSize = 1;
                  }
                  if (this._mode && this._mode.__creator == modeCreator) {
                      this._mode.init(this, iv && iv.words);
                  } else {
                      this._mode = modeCreator.call(mode, this, iv && iv.words);
                      this._mode.__creator = modeCreator;
                  }
              },
              _doProcessBlock: function (words, offset) {
                  this._mode.processBlock(words, offset);
              },
              _doFinalize: function () {
                  var finalProcessedBlocks;
                  var padding = this.cfg.padding;
                  if (this._xformMode == this._ENC_XFORM_MODE) {
                      padding.pad(this._data, this.blockSize);
                      finalProcessedBlocks = this._process(!!'flush');
                  } else {
                      finalProcessedBlocks = this._process(!!'flush');
                      padding.unpad(finalProcessedBlocks);
                  }
                  return finalProcessedBlocks;
              },
              blockSize: 128/32
          });
          var CipherParams = C_lib.CipherParams = Base.extend({
              init: function (cipherParams) {
                  this.mixIn(cipherParams);
              },
              toString: function (formatter) {
                  return (formatter || this.formatter).stringify(this);
              }
          });
          var C_format = C.format = {};
          var OpenSSLFormatter = C_format.OpenSSL = {
              stringify: function (cipherParams) {
                  var wordArray;
                  var ciphertext = cipherParams.ciphertext;
                  var salt = cipherParams.salt;
                  if (salt) {
                      wordArray = WordArray.create([0x53616c74, 0x65645f5f]).concat(salt).concat(ciphertext);
                  } else {
                      wordArray = ciphertext;
                  }
                  return wordArray.toString(Base64);
              },
              parse: function (openSSLStr) {
                  var salt;
                  var ciphertext = Base64.parse(openSSLStr);
                  var ciphertextWords = ciphertext.words;
                  if (ciphertextWords[0] == 0x53616c74 && ciphertextWords[1] == 0x65645f5f) {
                      salt = WordArray.create(ciphertextWords.slice(2, 4));
                      ciphertextWords.splice(0, 4);
                      ciphertext.sigBytes -= 16;
                  }
                  return CipherParams.create({ ciphertext: ciphertext, salt: salt });
              }
          };
          var SerializableCipher = C_lib.SerializableCipher = Base.extend({
              cfg: Base.extend({
                  format: OpenSSLFormatter
              }),
              encrypt: function (cipher, message, key, cfg) {
                  cfg = this.cfg.extend(cfg);
                  var encryptor = cipher.createEncryptor(key, cfg);
                  var ciphertext = encryptor.finalize(message);
                  var cipherCfg = encryptor.cfg;
                  return CipherParams.create({
                      ciphertext: ciphertext,
                      key: key,
                      iv: cipherCfg.iv,
                      algorithm: cipher,
                      mode: cipherCfg.mode,
                      padding: cipherCfg.padding,
                      blockSize: cipher.blockSize,
                      formatter: cfg.format
                  });
              },
              decrypt: function (cipher, ciphertext, key, cfg) {
                  cfg = this.cfg.extend(cfg);
                  ciphertext = this._parse(ciphertext, cfg.format);
                  var plaintext = cipher.createDecryptor(key, cfg).finalize(ciphertext.ciphertext);
                  return plaintext;
              },
              _parse: function (ciphertext, format) {
                  if (typeof ciphertext == 'string') {
                      return format.parse(ciphertext, this);
                  } else {
                      return ciphertext;
                  }
              }
          });
          var C_kdf = C.kdf = {};
          var OpenSSLKdf = C_kdf.OpenSSL = {
              execute: function (password, keySize, ivSize, salt, hasher) {
                  if (!salt) {
                      salt = WordArray.random(64/8);
                  }
                  if (!hasher) {
                      var key = EvpKDF.create({ keySize: keySize + ivSize }).compute(password, salt);
                  } else {
                      var key = EvpKDF.create({ keySize: keySize + ivSize, hasher: hasher }).compute(password, salt);
                  }
                  var iv = WordArray.create(key.words.slice(keySize), ivSize * 4);
                  key.sigBytes = keySize * 4;
                  return CipherParams.create({ key: key, iv: iv, salt: salt });
              }
          };
          var PasswordBasedCipher = C_lib.PasswordBasedCipher = SerializableCipher.extend({
              cfg: SerializableCipher.cfg.extend({
                  kdf: OpenSSLKdf
              }),
              encrypt: function (cipher, message, password, cfg) {
                  cfg = this.cfg.extend(cfg);
                  var derivedParams = cfg.kdf.execute(password, cipher.keySize, cipher.ivSize, cfg.salt, cfg.hasher);
                  cfg.iv = derivedParams.iv;
                  var ciphertext = SerializableCipher.encrypt.call(this, cipher, message, derivedParams.key, cfg);
                  ciphertext.mixIn(derivedParams);
                  return ciphertext;
              },
              decrypt: function (cipher, ciphertext, password, cfg) {
                  cfg = this.cfg.extend(cfg);
                  ciphertext = this._parse(ciphertext, cfg.format);
                  var derivedParams = cfg.kdf.execute(password, cipher.keySize, cipher.ivSize, ciphertext.salt, cfg.hasher);
                  cfg.iv = derivedParams.iv;
                  var plaintext = SerializableCipher.decrypt.call(this, cipher, ciphertext, derivedParams.key, cfg);
                  return plaintext;
              }
          });
      }());
    }));
    
    // --- CryptoJS AES Algorithm (aes.js) ---
    ;(function (root, factory, undef) {
      if (typeof exports === "object") {
        module.exports = exports = factory(root.CryptoJS || require("./core"), require("./enc-base64"), require("./md5"), require("./evpkdf"), require("./cipher-core"));
      }
      else if (typeof define === "function" && define.amd) {
        define(["./core", "./enc-base64", "./md5", "./evpkdf", "./cipher-core"], factory);
      }
      else {
        factory(root.CryptoJS);
      }
    }(this, function (CryptoJS) {
      (function () {
          var C = CryptoJS;
          var C_lib = C.lib;
          var BlockCipher = C_lib.BlockCipher;
          var C_algo = C.algo;
          var SBOX = [];
          var INV_SBOX = [];
          var SUB_MIX_0 = [];
          var SUB_MIX_1 = [];
          var SUB_MIX_2 = [];
          var SUB_MIX_3 = [];
          var INV_SUB_MIX_0 = [];
          var INV_SUB_MIX_1 = [];
          var INV_SUB_MIX_2 = [];
          var INV_SUB_MIX_3 = [];
          (function () {
              var d = [];
              for (var i = 0; i < 256; i++) {
                  if (i < 128) {
                      d[i] = i << 1;
                  } else {
                      d[i] = (i << 1) ^ 0x11b;
                  }
              }
              var x = 0;
              var xi = 0;
              for (var i = 0; i < 256; i++) {
                  var sx = xi ^ (xi << 1) ^ (xi << 2) ^ (xi << 3) ^ (xi << 4);
                  sx = (sx >>> 8) ^ (sx & 0xff) ^ 0x63;
                  SBOX[x] = sx;
                  INV_SBOX[sx] = x;
                  var x2 = d[x];
                  var x4 = d[x2];
                  var x8 = d[x4];
                  var t = (d[sx] * 0x101) ^ (sx * 0x1010100);
                  SUB_MIX_0[x] = (t << 24) | (t >>> 8);
                  SUB_MIX_1[x] = (t << 16) | (t >>> 16);
                  SUB_MIX_2[x] = (t << 8)  | (t >>> 24);
                  SUB_MIX_3[x] = t;
                  var t = (x8 * 0x1010101) ^ (x4 * 0x10001) ^ (x2 * 0x101) ^ (x * 0x1010100);
                  INV_SUB_MIX_0[sx] = (t << 24) | (t >>> 8);
                  INV_SUB_MIX_1[sx] = (t << 16) | (t >>> 16);
                  INV_SUB_MIX_2[sx] = (t << 8)  | (t >>> 24);
                  INV_SUB_MIX_3[sx] = t;
                  if (!x) {
                      x = xi = 1;
                  } else {
                      x = x2 ^ d[d[d[x8 ^ x2]]];
                      xi ^= d[d[xi]];
                  }
              }
          }());
          var RCON = [0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36];
          var AES = C_algo.AES = BlockCipher.extend({
              _doReset: function () {
                  var t;
                  if (this._nRounds && this._keyPriorReset === this._key) {
                      return;
                  }
                  var key = this._keyPriorReset = this._key;
                  var keyWords = key.words;
                  var keySize = key.sigBytes / 4;
                  var nRounds = this._nRounds = keySize + 6;
                  var ksRows = (nRounds + 1) * 4;
                  var keySchedule = this._keySchedule = [];
                  for (var ksRow = 0; ksRow < ksRows; ksRow++) {
                      if (ksRow < keySize) {
                          keySchedule[ksRow] = keyWords[ksRow];
                      } else {
                          t = keySchedule[ksRow - 1];
                          if (!(ksRow % keySize)) {
                              t = (t << 8) | (t >>> 24);
                              t = (SBOX[t >>> 24] << 24) | (SBOX[(t >>> 16) & 0xff] << 16) | (SBOX[(t >>> 8) & 0xff] << 8) | SBOX[t & 0xff];
                              t ^= RCON[(ksRow / keySize) | 0] << 24;
                          } else if (keySize > 6 && ksRow % keySize == 4) {
                              t = (SBOX[t >>> 24] << 24) | (SBOX[(t >>> 16) & 0xff] << 16) | (SBOX[(t >>> 8) & 0xff] << 8) | SBOX[t & 0xff];
                          }
                          keySchedule[ksRow] = keySchedule[ksRow - keySize] ^ t;
                      }
                  }
                  var invKeySchedule = this._invKeySchedule = [];
                  for (var invKsRow = 0; invKsRow < ksRows; invKsRow++) {
                      var ksRow = ksRows - invKsRow;
                      if (invKsRow % 4) {
                          var t = keySchedule[ksRow];
                      } else {
                          var t = keySchedule[ksRow - 4];
                      }
                      if (invKsRow < 4 || ksRow <= 4) {
                          invKeySchedule[invKsRow] = t;
                      } else {
                          invKeySchedule[invKsRow] = INV_SUB_MIX_0[SBOX[t >>> 24]] ^ INV_SUB_MIX_1[SBOX[(t >>> 16) & 0xff]] ^
                                                     INV_SUB_MIX_2[SBOX[(t >>> 8) & 0xff]] ^ INV_SUB_MIX_3[SBOX[t & 0xff]];
                      }
                  }
              },
              encryptBlock: function (M, offset) {
                  this._doCryptBlock(M, offset, this._keySchedule, SUB_MIX_0, SUB_MIX_1, SUB_MIX_2, SUB_MIX_3, SBOX);
              },
              decryptBlock: function (M, offset) {
                  var t = M[offset + 1];
                  M[offset + 1] = M[offset + 3];
                  M[offset + 3] = t;
                  this._doCryptBlock(M, offset, this._invKeySchedule, INV_SUB_MIX_0, INV_SUB_MIX_1, INV_SUB_MIX_2, INV_SUB_MIX_3, INV_SBOX);
                  var t = M[offset + 1];
                  M[offset + 1] = M[offset + 3];
                  M[offset + 3] = t;
              },
              _doCryptBlock: function (M, offset, keySchedule, SUB_MIX_0, SUB_MIX_1, SUB_MIX_2, SUB_MIX_3, SBOX) {
                  var nRounds = this._nRounds;
                  var s0 = M[offset]     ^ keySchedule[0];
                  var s1 = M[offset + 1] ^ keySchedule[1];
                  var s2 = M[offset + 2] ^ keySchedule[2];
                  var s3 = M[offset + 3] ^ keySchedule[3];
                  var ksRow = 4;
                  for (var round = 1; round < nRounds; round++) {
                      var t0 = SUB_MIX_0[s0 >>> 24] ^ SUB_MIX_1[(s1 >>> 16) & 0xff] ^ SUB_MIX_2[(s2 >>> 8) & 0xff] ^ SUB_MIX_3[s3 & 0xff] ^ keySchedule[ksRow++];
                      var t1 = SUB_MIX_0[s1 >>> 24] ^ SUB_MIX_1[(s2 >>> 16) & 0xff] ^ SUB_MIX_2[(s3 >>> 8) & 0xff] ^ SUB_MIX_3[s0 & 0xff] ^ keySchedule[ksRow++];
                      var t2 = SUB_MIX_0[s2 >>> 24] ^ SUB_MIX_1[(s3 >>> 16) & 0xff] ^ SUB_MIX_2[(s0 >>> 8) & 0xff] ^ SUB_MIX_3[s1 & 0xff] ^ keySchedule[ksRow++];
                      var t3 = SUB_MIX_0[s3 >>> 24] ^ SUB_MIX_1[(s0 >>> 16) & 0xff] ^ SUB_MIX_2[(s1 >>> 8) & 0xff] ^ SUB_MIX_3[s2 & 0xff] ^ keySchedule[ksRow++];
                      s0 = t0;
                      s1 = t1;
                      s2 = t2;
                      s3 = t3;
                  }
                  var t0 = ((SBOX[s0 >>> 24] << 24) | (SBOX[(s1 >>> 16) & 0xff] << 16) | (SBOX[(s2 >>> 8) & 0xff] << 8) | SBOX[s3 & 0xff]) ^ keySchedule[ksRow++];
                  var t1 = ((SBOX[s1 >>> 24] << 24) | (SBOX[(s2 >>> 16) & 0xff] << 16) | (SBOX[(s3 >>> 8) & 0xff] << 8) | SBOX[s0 & 0xff]) ^ keySchedule[ksRow++];
                  var t2 = ((SBOX[s2 >>> 24] << 24) | (SBOX[(s3 >>> 16) & 0xff] << 16) | (SBOX[(s0 >>> 8) & 0xff] << 8) | SBOX[s1 & 0xff]) ^ keySchedule[ksRow++];
                  var t3 = ((SBOX[s3 >>> 24] << 24) | (SBOX[(s0 >>> 16) & 0xff] << 16) | (SBOX[(s1 >>> 8) & 0xff] << 8) | SBOX[s2 & 0xff]) ^ keySchedule[ksRow++];
                  M[offset]     = t0;
                  M[offset + 1] = t1;
                  M[offset + 2] = t2;
                  M[offset + 3] = t3;
              },
              keySize: 256/32
          });
          C.AES = BlockCipher._createHelper(AES);
      }());
      return CryptoJS.AES;
    }));
    
    // ----- Minimal CryptoJS Library End -----
    
    
    
    

Building Your Applications on Top of DeepCura

While the integration flow remains the same as in the Google Spreadsheet example, you aren't limited to that endpoint. Instead, you can direct the JSON payload to your own application by following these steps:

  • 1. Specify Your Own Endpoint
    In the gridhook modal. This tells DeepCura where to send the JSON payload after processing a note or transcript.

    2. Direct the JSON Payload
    Once your endpoint is configured, DeepCura will send the encrypted JSON payload to your URL. Your application should be set up to receive and process these incoming HTTP requests (for example, by exposing an endpoint that accepts POST requests).

    3. Decrypting the Incoming Payload
    Since the payload is encrypted for security and HIPAA compliance, you will need to decrypt it on your end using your secret key.

    • import base64
      from cryptography.hazmat.primitives import padding, algorithms
      from cryptography.hazmat.primitives.ciphers import Cipher, modes
      from cryptography.hazmat.backends import default_backend
      
      def decrypt_with_aes(encrypted_text: str, secret_key: str):
          try:
              # Split the encrypted text to get IV and ciphertext
              iv_base64, encrypted_base64 = encrypted_text.split(':')
              
              # Decode Base64
              iv = base64.b64decode(iv_base64)
              encrypted = base64.b64decode(encrypted_base64)
              
              # Ensure the key is 32 bytes (AES-256)
              key = secret_key.encode('utf-8').ljust(32, b'\0')[:32]
              
              # Create cipher
              cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
              decryptor = cipher.decryptor()
              
              # Decrypt the ciphertext
              padded_text = decryptor.update(encrypted) + decryptor.finalize()
              
              # Unpad the plaintext
              unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
              plaintext = unpadder.update(padded_text) + unpadder.finalize()
              
              return plaintext.decode('utf-8')
          except Exception as e:
              print(f"Error during AES decryption: {e}")
              return None
    • Here is an example of how the JSON payload will look before the decryption process:

      {
        "data": "IV_SCHEMA_BASE64:ENCRYPTED_SCHEMA_BASE64",
        "providerId": "user_12345",
        "providerName": "Dr. Smith",
        "noteId": "1617181920",
        "sourceTranscript": "IV_TRANSCRIPT_BASE64:ENCRYPTED_TRANSCRIPT_BASE64",
        "sourceAINote": "IV_AINOTE_BASE64:ENCRYPTED_AINOTE_BASE64",
        "template": "Billing Template: [template details removed]"
      }

Integration Best Practices

  1. Security: Make sure that your endpoint is secure (for example, use HTTPS) and that your secret key remains confidential.
  2. Data Validation: Once decrypted, validate the JSON data before processing it further.
  3. Scalability: If your application expects a high volume of requests, consider implementing proper load balancing and error handling to manage traffic effectively.

By setting your own endpoint and using the decryption function above, you can easily adapt DeepCura's AI Gridhooks to fit into your existing systems — whether that's for clinical documentation, research analytics, or any other data-driven application.

Feel free to customize and extend this integration to suit your unique requirements!

Quick Tips

  • Start with the Google Sheets Gridhook integration — it is the easiest to set up and provides immediate value for tracking encounter data.
  • If your EHR is not in the supported list, use AI Gridhooks to bridge the gap with custom webhook integrations.
  • Contact DeepCura support if you need help building custom Gridhook integrations.

Next: Lesson 7.4 — Billing & Subscription


Next Steps

Continue to Lesson 7.4 — Billing & Subscription