We just wrapped up a three-month pilot where we automated the ingestion of Otter.ai meeting transcripts into Salesforce Cases. Goal was to attach customer call intelligence directly to support records without manual copy-paste. The business logic was sound, but the integration path had a few hidden potholes.
Here's the architecture that finally worked reliably. We used Otter's email export as the trigger, as their API is better suited for bulk export than real-time. This is a middleware approach, not a direct Point-and-Click integration.
**Components:**
* Otter.ai (Business plan, for email export)
* A simple AWS Lambda (Python 3.9)
* Salesforce Cases with a custom field `Transcript_Link__c`
* Salesforce Files (for attachment storage)
**Step-by-step Flow:**
1. **Configure Otter Export:** In Otter, set up automated email export for specific meeting groups or use the share link post-meeting. The email goes to a dedicated mailbox we monitor via SES.
2. **Lambda Trigger:** SES forwards the email to a Lambda function. The function parses the email body for the Otter share link and meeting title.
3. **Salesforce Integration:** Lambda uses the Salesforce REST API to:
* Query for the Case based on the meeting title (we prefix our support calls with Case ID).
* If found, it creates a `ContentVersion` record (Salesforce File) linking the Otter share URL as an external document, and attaches it to the Case.
**Critical Code Snippet (Lambda - Python):**
The key is extracting the link from Otter's email format and handling Salesforce authentication securely.
```python
import re
import boto3
from simple_salesforce import Salesforce
def lambda_handler(event, context):
# Parse SES event for email body
raw_message = event['content']
# Otter's share link pattern
link_pattern = r'https://otter.ai/[^s]+'
otter_link = re.search(link_pattern, raw_message).group(0)
# Extract potential Case ID from email subject
# ... subject parsing logic ...
sf = Salesforce(
instance_url = os.environ['SF_INSTANCE'],
session_id = os.environ['SF_TOKEN']
)
# Query Case
case_query = f"SELECT Id FROM Case WHERE CaseNumber = '{case_number}'"
case_result = sf.query(case_query)
if case_result['totalSize'] > 0:
case_id = case_result['records'][0]['Id']
# Create ContentVersion (File) record linking to Otter URL
file_record = {
'Title': f'Otter Transcript - {meeting_title}',
'PathOnClient': 'otter_transcript.url',
'VersionData': f'[InternetShortcut]nURL={otter_link}',
'FirstPublishLocationId': case_id
}
sf.ContentVersion.create(file_record)
```
**Pitfalls & Lessons:**
* **Rate Limits:** Otter's API has strict limits. Email export was more reliable for our volume (~50 calls/day).
* **Auth:** Use a dedicated Salesforce Integration User with a limited permission set (Create on `ContentVersion`, Read on `Case`).
* **Data Mapping:** Don't rely on fuzzy title matching. We enforce a naming convention `[CaseID] - Client Name` in the Otter meeting title.
* **Cost:** Storing full transcripts in Salesforce Files as text blobs is expensive. We store the link, not the full text. If you need full text search, consider an external store and link to it.
This took our L2 support team from manually hunting for transcripts to having them one click away in the Case feed. TTR improved by about 15% for relevant cases.
-shift
shift left or go home
Interesting approach using email as the trigger. It's a clever workaround for their API limitations, but you're definitely adding a layer of complexity with the SES mailbox setup.
The biggest challenge we saw in a similar project was reliably parsing the Otter share link from the email body, especially as their email templates can change. Did you have to build in any redundancy or fallback logic for that parsing step?
Also, curious how you're handling data retention and compliance. Storing the transcript as a Salesforce File means it's subject to your Salesforce data governance policies, which is good, but have you considered the customer's right to be forgotten? If a transcript is deleted in Otter, your Lambda would need a separate process to purge the copy in Salesforce Files.