The Recall AI SDK provides methods to control bot recording operations during and after meetings.
Recording lifecycle
Recordings go through several phases:
Bot joins meeting
The bot enters the meeting and waits for recording permission.
Recording starts
Once permission is granted (or based on start_recording_on setting), recording begins automatically.
Recording active
The bot captures video, audio, and transcripts in real-time.
Recording stops
Recording ends when you call stopRecording(), when the meeting ends, or when the bot leaves.
Media available
Recording files are processed and made available for download.
Automatic recording control
Control when recording begins using recording_mode_options:
Start on join
Manual start
const bot = await recall . bot . create ({
meeting_url: 'https://zoom.us/j/123456789' ,
recording_mode_options: {
start_recording_on: 'bot_join'
}
});
The bot starts recording immediately after joining the meeting. const bot = await recall . bot . create ({
meeting_url: 'https://zoom.us/j/123456789' ,
recording_mode_options: {
start_recording_on: 'manual'
}
});
You control when recording starts (requires additional API calls).
Stopping recordings
Stop a bot’s recording while keeping it in the meeting:
await recall . bot . stopRecording ({ id: 'bot-id' });
After stopping the recording, the bot remains in the meeting with status in_call_not_recording. The recorded media is still processed and available.
When to stop recording
Confidential discussion Stop recording when sensitive topics arise that shouldn’t be captured.
Meeting breaks Pause recording during breaks to save storage and processing costs.
Partial recording Record only specific segments of longer meetings.
Resource management Stop early if you’ve captured what you need to optimize resource usage.
Example: Stop after time limit
const bot = await recall . bot . create ({
meeting_url: 'https://meet.google.com/abc-defg-hij' ,
bot_name: 'Time-Limited Recorder'
});
// Stop recording after 30 minutes
setTimeout ( async () => {
const botInfo = await recall . bot . retrieve ({ id: bot . id });
if ( botInfo . status === 'in_call_recording' ) {
await recall . bot . stopRecording ({ id: bot . id });
console . log ( 'Recording stopped after 30 minutes' );
}
}, 30 * 60 * 1000 );
Leaving the call
Make the bot leave the meeting entirely:
await recall . bot . leaveCall ({ id: 'bot-id' });
Leaving the call stops the recording and removes the bot from the meeting. The bot cannot rejoin the same meeting after leaving.
Stop recording vs. leave call
Action Recording Bot in meeting Re-recording possible stopRecording()Stops Remains Yes (via API) leaveCall()Stops Leaves No
Example: Conditional leave
async function leaveIfNoParticipants ( botId ) {
const botInfo = await recall . bot . retrieve ({ id: botId });
// Check participant count from bot intelligence
if ( botInfo . participant_count <= 1 ) {
await recall . bot . leaveCall ({ id: botId });
console . log ( 'Bot left - no other participants' );
}
}
// Check every 5 minutes
setInterval (() => leaveIfNoParticipants ( 'bot-id' ), 5 * 60 * 1000 );
Delete recorded media to free up storage:
await recall . bot . deleteBotMedia ({ id: 'bot-id' });
Deleting media is permanent and irreversible . Make sure you’ve downloaded or processed any needed content first.
What gets deleted
When you delete bot media, the following are removed:
Video recordings
Audio files
Raw transcripts
Chat messages (if captured)
Screen share recordings
Bot metadata, intelligence summaries, and logs are not deleted. You can still retrieve bot information and status history.
After processing
Retention policy
On error
User request
async function processAndCleanup ( botId ) {
// Wait for recording to complete
let status = await waitForCompletion ( botId );
if ( status === 'done' ) {
// Get transcript and intelligence
const transcript = await recall . bot . getTranscript ({ id: botId });
const intel = await recall . bot . getBotIntellienge ({ id: botId });
// Store in your database
await saveToDatabase ({ transcript , intel });
// Delete media from Recall
await recall . bot . deleteBotMedia ({ id: botId });
console . log ( 'Media deleted after processing' );
}
}
async function enforceRetentionPolicy () {
const cutoffDate = new Date ();
cutoffDate . setDate ( cutoffDate . getDate () - 30 ); // 30 days ago
const oldBots = await recall . bot . list ({
join_at_before: cutoffDate ,
status: 'done'
});
for ( const bot of oldBots ) {
await recall . bot . deleteBotMedia ({ id: bot . id });
console . log ( `Deleted media for bot ${ bot . id } ` );
}
}
// Run daily
setInterval ( enforceRetentionPolicy , 24 * 60 * 60 * 1000 );
async function cleanupFailedBot ( botId ) {
const botInfo = await recall . bot . retrieve ({ id: botId });
if ( botInfo . status === 'fatal' ) {
// Bot failed, no useful media
await recall . bot . deleteBotMedia ({ id: botId });
console . log ( 'Cleaned up failed bot media' );
}
}
async function handleDeleteRequest ( userId , botId ) {
// Verify user owns this bot
const bot = await recall . bot . retrieve ({ id: botId });
if ( bot . user_id !== userId ) {
throw new Error ( 'Unauthorized' );
}
// Delete from Recall
await recall . bot . deleteBotMedia ({ id: botId });
// Update your database
await db . bots . update ( botId , { media_deleted: true });
console . log ( `Media deleted per user ${ userId } request` );
}
Recording modes
Configure how the bot records meetings:
Speaker view
Focus on the active speaker:
const bot = await recall . bot . create ({
meeting_url: 'https://zoom.us/j/123456789' ,
recording_mode: 'speaker_view' ,
recording_mode_options: {
participant_video_when_screenshare: 'show' // or 'hide'
}
});
Gallery view
Capture all participants:
const bot = await recall . bot . create ({
meeting_url: 'https://teams.microsoft.com/l/meetup-join/...' ,
recording_mode: 'gallery_view'
});
Audio only
Record only audio without video:
const bot = await recall . bot . create ({
meeting_url: 'https://meet.google.com/abc-defg-hij' ,
recording_mode: 'audio_only'
});
Audio-only mode uses less bandwidth and storage, making it ideal for podcast recordings or when video isn’t needed.
Screen share handling
Control how screen shares are recorded:
const bot = await recall . bot . create ({
meeting_url: 'https://zoom.us/j/123456789' ,
recording_mode: 'speaker_view' ,
recording_mode_options: {
participant_video_when_screenshare: 'hide'
}
});
When someone shares their screen, participant videos are hidden and only the screen share is recorded. Best for: Presentations, demos, screen-focused meetings
When someone shares their screen, participant videos remain visible alongside the screen share. Best for: Collaborative work sessions, code reviews
Monitoring recording status
Track recording progress in real-time:
async function monitorRecording ( botId ) {
const checkStatus = async () => {
const bot = await recall . bot . retrieve ({ id: botId });
console . log ( `Status: ${ bot . status } ` );
switch ( bot . status ) {
case 'in_call_recording' :
console . log ( '✓ Recording in progress' );
break ;
case 'recording_permission_denied' :
console . error ( '✗ Recording permission denied' );
await recall . bot . leaveCall ({ id: botId });
break ;
case 'in_call_not_recording' :
console . log ( '⊗ In call but not recording' );
break ;
case 'recording_done' :
console . log ( '✓ Recording complete' );
return true ; // Stop monitoring
case 'fatal' :
console . error ( '✗ Fatal error occurred' );
return true ; // Stop monitoring
}
return false ; // Continue monitoring
};
// Poll every 10 seconds
const interval = setInterval ( async () => {
const done = await checkStatus ();
if ( done ) clearInterval ( interval );
}, 10000 );
}
monitorRecording ( 'bot-id' );
Best practices
Check permissions Always monitor for recording_permission_denied status and handle appropriately.
Implement retention Delete media after your retention period to minimize storage costs.
Backup before delete Download or process media before calling deleteBotMedia().
Handle errors gracefully Clean up media from failed bots to avoid unnecessary storage charges.
Complete workflow example
import { Recall } from '@recall-ai/sdk' ;
import fs from 'fs' ;
const recall = new Recall ({
apiKey: process . env . RECALL_API_KEY ,
region: 'us-west-2'
});
async function recordAndCleanup ( meetingUrl ) {
let botId ;
try {
// Create bot with specific recording settings
const bot = await recall . bot . create ({
meeting_url: meetingUrl ,
bot_name: 'Auto-Cleanup Recorder' ,
recording_mode: 'speaker_view' ,
recording_mode_options: {
start_recording_on: 'bot_join' ,
participant_video_when_screenshare: 'hide'
}
});
botId = bot . id ;
console . log ( `Bot created: ${ botId } ` );
// Monitor until recording is done
let status = 'ready' ;
while ( ! [ 'done' , 'fatal' , 'recording_done' ]. includes ( status )) {
await new Promise ( resolve => setTimeout ( resolve , 10000 ));
const botInfo = await recall . bot . retrieve ({ id: botId });
status = botInfo . status ;
console . log ( `Current status: ${ status } ` );
// Stop recording after 60 minutes
if ( status === 'in_call_recording' ) {
// Implement your logic here
}
}
// Get transcript and intelligence
if ( status === 'done' ) {
const transcript = await recall . bot . getTranscript ({
id: botId ,
enhanced_diarization: true
});
const intelligence = await recall . bot . getBotIntellienge ({ id: botId });
// Save to local storage
fs . writeFileSync (
`recording- ${ botId } .json` ,
JSON . stringify ({ transcript , intelligence }, null , 2 )
);
console . log ( 'Data saved locally' );
// Delete media from Recall
await recall . bot . deleteBotMedia ({ id: botId });
console . log ( 'Media deleted from Recall' );
}
} catch ( error ) {
console . error ( 'Error:' , error );
// Clean up on error
if ( botId ) {
try {
await recall . bot . leaveCall ({ id: botId });
await recall . bot . deleteBotMedia ({ id: botId });
} catch ( cleanupError ) {
console . error ( 'Cleanup error:' , cleanupError );
}
}
}
}
recordAndCleanup ( 'https://zoom.us/j/123456789' );