Building healthcare applications requires careful attention to security. This guide covers the essential security practices for integrating with Sully.ai, from API key management to handling protected health information (PHI).
API Key Management
Your API key is the gateway to your Sully.ai account. Protecting it is critical to preventing unauthorized access.
Never Hardcode Keys
Never commit API keys to source control or include them in client-side code.
TypeScript (Bad)
Python (Bad)
// NEVER do this
const sully = new Sully ({
apiKey: 'sk_live_abc123def456' , // Exposed in source code!
accountId: 'acc_xyz789' ,
});
Use Environment Variables
For local development and simple deployments, use environment variables:
import Sully from '@sullyai/sullyai' ;
const sully = new Sully ({
apiKey: process . env . SULLY_API_KEY ,
accountId: process . env . SULLY_ACCOUNT_ID ,
});
// Validate environment variables at startup
if ( ! process . env . SULLY_API_KEY || ! process . env . SULLY_ACCOUNT_ID ) {
throw new Error ( 'Missing required Sully.ai credentials' );
}
Use a Secrets Manager for Production
For production deployments, use a dedicated secrets manager:
TypeScript (AWS Secrets Manager)
Python (AWS Secrets Manager)
import {
SecretsManagerClient ,
GetSecretValueCommand ,
} from '@aws-sdk/client-secrets-manager' ;
import Sully from '@sullyai/sullyai' ;
interface SullySecrets {
apiKey : string ;
accountId : string ;
}
async function getSullyClient () : Promise < Sully > {
const client = new SecretsManagerClient ({ region: 'us-east-1' });
const response = await client . send (
new GetSecretValueCommand ({ SecretId: 'sully-api-credentials' })
);
const secrets : SullySecrets = JSON . parse ( response . SecretString ! );
return new Sully ({
apiKey: secrets . apiKey ,
accountId: secrets . accountId ,
});
}
Other supported secrets managers include:
HashiCorp Vault
Google Cloud Secret Manager
Azure Key Vault
Doppler
Key Rotation and Environment Separation
Practice Description Rotate keys periodically Rotate API keys every 90 days or after personnel changes Separate environments Use different API keys for development, staging, and production Limit access Only give API keys to team members who need them Audit usage Regularly review API key usage in the Sully Dashboard
Webhook Verification
If your integration receives webhooks , always verify the signature before processing events.
Every webhook includes an x-sully-signature header:
x-sully-signature: t=1706123456,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
Component Description tUnix timestamp when the request was signed v1HMAC-SHA256 signature of the payload
Verification Requirements
Always verify both the signature AND the timestamp. Skipping either check leaves your endpoint vulnerable.
Verify the signature - Compute HMAC-SHA256 and compare using constant-time comparison
Check the timestamp - Reject requests older than 5 minutes to prevent replay attacks
Verification Implementation
import * as crypto from 'crypto' ;
function verifyWebhookSignature (
signatureHeader : string ,
rawBody : string ,
secret : string
) : boolean {
// Parse header components
const parts : Record < string , string > = {};
for ( const part of signatureHeader . split ( ',' )) {
const [ key , value ] = part . split ( '=' );
if ( key && value ) parts [ key . trim ()] = value . trim ();
}
const { t : timestamp , v1 : signature } = parts ;
if ( ! timestamp || ! signature ) return false ;
// Reject requests older than 5 minutes (replay protection)
const currentTime = Math . floor ( Date . now () / 1000 );
if ( Math . abs ( currentTime - parseInt ( timestamp , 10 )) > 300 ) {
console . error ( 'Webhook timestamp expired' );
return false ;
}
// Compute expected signature
const signedPayload = ` ${ timestamp } . ${ rawBody } ` ;
const expectedSignature = crypto
. createHmac ( 'sha256' , secret )
. update ( signedPayload , 'utf8' )
. digest ( 'hex' );
// Constant-time comparison to prevent timing attacks
return crypto . timingSafeEqual (
Buffer . from ( signature , 'hex' ),
Buffer . from ( expectedSignature , 'hex' )
);
}
For complete webhook handling examples including event processing, see the Webhooks Guide .
Data in Transit
All communication with Sully.ai is encrypted in transit.
HTTPS Required
All API calls must use HTTPS (TLS 1.2 or higher)
HTTP requests are rejected
WebSocket connections must use WSS (WebSocket Secure)
// Correct: HTTPS
const BASE_URL = 'https://api.sully.ai' ;
// Correct: WSS for streaming
const WS_URL = 'wss://api.sully.ai/stream' ;
// WRONG: Never use HTTP
// const BASE_URL = 'http://api.sully.ai'; // Will be rejected
Certificate Validation
Always validate TLS certificates. Never disable certificate verification, even in development:
// The SDK handles certificate validation automatically
// Never override this behavior
Access Control
Sully.ai uses account-based isolation to separate data between customers.
Account Isolation
Concept Description Account ID Identifies your organization; all your data is isolated to this account API Key Authenticates requests; scoped to a single account Resource IDs Transcriptions, notes, etc. are only accessible within your account
Multi-Tenant Patterns
If you’re building a multi-tenant application, consider these patterns:
Option 1: Separate API Keys
Request a separate API key for each tenant from Sully.ai. This provides the strongest isolation.
// Each tenant has their own credentials
const tenantClients = new Map < string , Sully >();
function getClientForTenant ( tenantId : string ) : Sully {
if ( ! tenantClients . has ( tenantId )) {
const credentials = getTenantCredentials ( tenantId );
tenantClients . set (
tenantId ,
new Sully ({
apiKey: credentials . apiKey ,
accountId: credentials . accountId ,
})
);
}
return tenantClients . get ( tenantId ) ! ;
}
Option 2: Your Own Auth Layer
Use a single Sully.ai account and implement your own authorization layer to control which tenants can access which resources.
// Map Sully resources to your tenants
interface ResourceMapping {
sullyResourceId : string ;
tenantId : string ;
createdAt : Date ;
}
// Verify tenant access before returning data
async function getNote ( noteId : string , requestingTenantId : string ) {
const mapping = await db . resourceMappings . findOne ({ sullyResourceId: noteId });
if ( mapping ?. tenantId !== requestingTenantId ) {
throw new Error ( 'Unauthorized' );
}
return sully . notes . get ( noteId );
}
Handling PHI
When working with protected health information (PHI), follow these practices to maintain compliance.
Data Minimization
Only send data that’s necessary for the operation:
// Good: Send only the required transcript
await sully . notes . create ({
transcript: transcription ,
noteType: { type: 'soap' },
});
// Avoid: Don't include unnecessary patient identifiers
// in metadata or other fields
Logging Practices
Never log full transcripts, clinical notes, or other PHI in your application logs.
// Good: Log resource IDs only
console . log ( `Note generated: ${ noteId } ` );
// Bad: Never log PHI
// console.log(`Note content: ${note.payload.markdown}`); // DON'T DO THIS
// Good: If you must log for debugging, redact sensitive data
function sanitizeForLogging ( data : unknown ) : string {
// Implement redaction logic
return JSON . stringify ( data ). replace ( /transcript | note | content/ gi , '[REDACTED]' );
}
Webhook Endpoint Security
Secure your webhook endpoint to protect incoming PHI:
Use HTTPS only
Verify webhook signatures (see above)
Process events asynchronously and acknowledge quickly
Store received data in encrypted storage
Implement access controls on your endpoint
Data Retention
Consider implementing data retention policies:
Delete transcriptions and notes from Sully.ai when no longer needed
Use the DELETE endpoints to remove resources
Document your retention policies for compliance audits
// Delete resources when no longer needed
await sully . audio . transcriptions . delete ( transcriptionId );
await sully . notes . delete ( noteId );
Incident Response
If you suspect your API key has been compromised, act immediately.
Key Compromise Response
Revoke the key immediately - Go to the Sully Dashboard and revoke the compromised key
Generate a new key - Create a new API key in the dashboard
Update all services - Deploy the new key to all applications using the compromised key
Audit recent activity - Review API logs in the dashboard for unauthorized access
Investigate the breach - Determine how the key was exposed and fix the vulnerability
For security concerns or to report a potential breach, contact the Sully.ai security team:
Security Checklist
Use this checklist before deploying to production:
API keys stored in environment variables or secrets manager
No API keys in source code or version control
Webhook signature verification enabled
Webhook timestamp validation enabled (5-minute window)
All API connections use HTTPS
All WebSocket connections use WSS
No PHI in application logs
Key rotation process documented
Incident response plan in place
Data retention policy defined
Webhook endpoint secured with HTTPS
Different API keys for dev/staging/production
Next Steps