Skip to main content
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.
// 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:
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

PracticeDescription
Rotate keys periodicallyRotate API keys every 90 days or after personnel changes
Separate environmentsUse different API keys for development, staging, and production
Limit accessOnly give API keys to team members who need them
Audit usageRegularly review API key usage in the Sully Dashboard

Webhook Verification

If your integration receives webhooks, always verify the signature before processing events.

Signature Format

Every webhook includes an x-sully-signature header:
x-sully-signature: t=1706123456,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
ComponentDescription
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.
  1. Verify the signature - Compute HMAC-SHA256 and compare using constant-time comparison
  2. 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

ConceptDescription
Account IDIdentifies your organization; all your data is isolated to this account
API KeyAuthenticates requests; scoped to a single account
Resource IDsTranscriptions, 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

  1. Revoke the key immediately - Go to the Sully Dashboard and revoke the compromised key
  2. Generate a new key - Create a new API key in the dashboard
  3. Update all services - Deploy the new key to all applications using the compromised key
  4. Audit recent activity - Review API logs in the dashboard for unauthorized access
  5. Investigate the breach - Determine how the key was exposed and fix the vulnerability

Contact Support

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