Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions packages/twenty-apps/public/call-recorder/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,15 @@ Applications → Call Recorder):

| Server variable | Required | Purpose |
|---|---|---|
| `RECALL_API_KEY` | Yes | Recall.ai API key for the configured region; used to schedule, update, and cancel bots. |
| `CALL_RECORDER_PROVIDER` | No | Recording provider. Use `recall` (default) or `meeting-baas`. |
| `RECALL_API_KEY` | Provider | Recall.ai API key for the configured region; required when `CALL_RECORDER_PROVIDER` is `recall`. |
| `RECALL_REGION` | No | Recall.ai region for API requests. Defaults to `eu-central-1` (Europe / Frankfurt). |
| `CALL_RECORDER_RECORDING_RETENTION_HOURS` | No | How long Recall.ai retains the source media after processing. Defaults to `166` hours (6 days 22 hours), just under Recall's 168-hour free-storage window. Values above `168` may incur Recall storage charges. Twenty's ingested copy is unaffected. |
| `RECALL_WEBHOOK_SECRET` | Yes | Svix signing secret (`whsec_…`) used to verify incoming Recall webhooks. |
| `RECALL_WEBHOOK_SECRET` | Provider | Svix signing secret (`whsec_…`) used to verify incoming Recall webhooks; required when `CALL_RECORDER_PROVIDER` is `recall`. |
| `MEETING_BAAS_API_KEY` | Provider | Meeting BaaS API key; required when `CALL_RECORDER_PROVIDER` is `meeting-baas`. A Sending Access key can schedule bots; Full Access is needed for cancellation and reconciliation. |
| `MEETING_BAAS_API_BASE_URL` | No | Meeting BaaS API base URL. Defaults to `https://api.meetingbaas.com`. |
| `MEETING_BAAS_CALLBACK_URL` | Provider | Absolute Twenty server webhook URL for `meeting-baas-webhook`; required when `CALL_RECORDER_PROVIDER` is `meeting-baas`. |
| `MEETING_BAAS_CALLBACK_SECRET` | Provider | Shared secret sent by Meeting BaaS in the `x-mb-secret` callback header; required when `CALL_RECORDER_PROVIDER` is `meeting-baas`. |

### Application variables

Expand Down Expand Up @@ -171,3 +176,28 @@ ID is the **Recall webhook logic function**.
3. Set it as the `RECALL_WEBHOOK_SECRET` server variable on the
**Call Recorder** application registration.
4. Set `RECALL_API_KEY` (and optionally `RECALL_REGION`) the same way.

### Configuring Meeting BaaS

Set `CALL_RECORDER_PROVIDER=meeting-baas`, then set `MEETING_BAAS_API_KEY`,
`MEETING_BAAS_CALLBACK_URL`, and `MEETING_BAAS_CALLBACK_SECRET`.

Use this URL on your deployment for `MEETING_BAAS_CALLBACK_URL`, replacing only
the host:

```text
https://<your-twenty-host>/webhooks/server/8da4b8b5-5edf-4880-b51f-ab6e679ec617/7a30ca04-7e6e-4a85-96d0-7e16adc3e2f1
```

The app passes this URL to Meeting BaaS in each scheduled bot's
`callback_config`. Meeting BaaS should send completed and failed bot callbacks
with the app metadata in `data.extra`; the Twenty webhook resolver reads
`data.extra.twentyWorkspaceId`.

Meeting BaaS docs:

- API v2: https://docs.meetingbaas.com/api-v2
- Scheduled bots: https://docs.meetingbaas.com/api-v2/reference/bots/createScheduledBot
- Artifacts and callbacks: https://docs.meetingbaas.com/api-v2/getting-started/getting-the-data
- Bot status: https://docs.meetingbaas.com/api-v2/reference/bots/getBotStatus
- API keys: https://docs.meetingbaas.com/api-v2/api-keys
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { CALL_RECORDER_JOIN_EARLY_MINUTES_ENV_VAR_NAME } from 'src/logic-functio
import { CALL_RECORDER_NAME_ENV_VAR_NAME } from 'src/logic-functions/constants/call-recorder-name-env-var-name';
import { CALL_RECORDER_NOONE_JOINED_TIMEOUT_SECONDS } from 'src/logic-functions/constants/call-recorder-noone-joined-timeout-seconds';
import { CALL_RECORDER_NOONE_JOINED_TIMEOUT_SECONDS_ENV_VAR_NAME } from 'src/logic-functions/constants/call-recorder-noone-joined-timeout-seconds-env-var-name';
import { CALL_RECORDER_PROVIDER_ENV_VAR_NAME } from 'src/logic-functions/constants/call-recorder-provider-env-var-name';
import { CALL_RECORDER_RECORDING_RETENTION_HOURS_ENV_VAR_NAME } from 'src/logic-functions/constants/call-recorder-recording-retention-hours-env-var-name';
import { CALL_RECORDER_WAITING_ROOM_TIMEOUT_SECONDS } from 'src/logic-functions/constants/call-recorder-waiting-room-timeout-seconds';
import { CALL_RECORDER_WAITING_ROOM_TIMEOUT_SECONDS_ENV_VAR_NAME } from 'src/logic-functions/constants/call-recorder-waiting-room-timeout-seconds-env-var-name';
Expand All @@ -24,6 +25,10 @@ import { DEFAULT_RECALL_REGION } from 'src/logic-functions/constants/default-rec
import { RECALL_API_KEY_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-api-key-env-var-name';
import { RECALL_REGION_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-region-env-var-name';
import { RECALL_WEBHOOK_SECRET_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-webhook-secret-env-var-name';
import { MEETING_BAAS_API_BASE_URL_ENV_VAR_NAME } from 'src/logic-functions/constants/meeting-baas-api-base-url-env-var-name';
import { MEETING_BAAS_API_KEY_ENV_VAR_NAME } from 'src/logic-functions/constants/meeting-baas-api-key-env-var-name';
import { MEETING_BAAS_CALLBACK_SECRET_ENV_VAR_NAME } from 'src/logic-functions/constants/meeting-baas-callback-secret-env-var-name';
import { MEETING_BAAS_CALLBACK_URL_ENV_VAR_NAME } from 'src/logic-functions/constants/meeting-baas-callback-url-env-var-name';

export default defineApplication({
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
Expand Down Expand Up @@ -72,11 +77,15 @@ export default defineApplication({
},
},
serverVariables: {
[CALL_RECORDER_PROVIDER_ENV_VAR_NAME]: {
description:
'Recording provider to use. Supported values: recall, meeting-baas. Defaults to recall when unset.',
isSecret: false,
},
[RECALL_API_KEY_ENV_VAR_NAME]: {
description:
'Recall.ai API key for the configured region. Set by the server admin on this registration after installation; used to create, update, and cancel scheduled recording bots.',
'Recall.ai API key for the configured region. Required when CALL_RECORDER_PROVIDER is recall.',
isSecret: true,
isRequired: true,
},
[RECALL_REGION_ENV_VAR_NAME]: {
description: `Recall.ai region used for API requests. Defaults to ${DEFAULT_RECALL_REGION} when unset. Europe Frankfurt is eu-central-1.`,
Expand All @@ -88,9 +97,28 @@ export default defineApplication({
},
[RECALL_WEBHOOK_SECRET_ENV_VAR_NAME]: {
description:
'Recall.ai webhook signing secret (whsec_...). Set by the server admin from the Recall webhook endpoint settings; used to verify the Svix signature of incoming Recall webhook deliveries.',
'Recall.ai webhook signing secret (whsec_...). Required when CALL_RECORDER_PROVIDER is recall.',
isSecret: true,
},
[MEETING_BAAS_API_KEY_ENV_VAR_NAME]: {
description:
'Meeting BaaS API key. Required when CALL_RECORDER_PROVIDER is meeting-baas.',
isSecret: true,
},
[MEETING_BAAS_API_BASE_URL_ENV_VAR_NAME]: {
description:
'Meeting BaaS API base URL. Defaults to https://api.meetingbaas.com when unset.',
isSecret: false,
},
[MEETING_BAAS_CALLBACK_URL_ENV_VAR_NAME]: {
description:
'Absolute Twenty server webhook URL for the meeting-baas-webhook logic function. Required when CALL_RECORDER_PROVIDER is meeting-baas.',
isSecret: false,
},
[MEETING_BAAS_CALLBACK_SECRET_ENV_VAR_NAME]: {
description:
'Shared secret sent by Meeting BaaS in the x-mb-secret header. Required when CALL_RECORDER_PROVIDER is meeting-baas.',
isSecret: true,
isRequired: true,
},
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const MEETING_BAAS_WEBHOOK_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER =
'7a30ca04-7e6e-4a85-96d0-7e16adc3e2f1';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const CALL_RECORDER_PROVIDER_ENV_VAR_NAME = 'CALL_RECORDER_PROVIDER';
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const MEETING_BAAS_API_BASE_URL_ENV_VAR_NAME =
'MEETING_BAAS_API_BASE_URL';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const MEETING_BAAS_API_KEY_ENV_VAR_NAME = 'MEETING_BAAS_API_KEY';
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const MEETING_BAAS_CALLBACK_SECRET_ENV_VAR_NAME =
'MEETING_BAAS_CALLBACK_SECRET';
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const MEETING_BAAS_CALLBACK_URL_ENV_VAR_NAME =
'MEETING_BAAS_CALLBACK_URL';
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { CallRecordingStatus } from 'src/logic-functions/constants/call-recording-status';

export const mapMeetingBaasStatusToCallRecordingStatus = (
status: string | undefined,
): CallRecordingStatus | undefined => {
switch (status) {
case 'queued':
return CallRecordingStatus.SCHEDULED;
case 'joining':
return CallRecordingStatus.JOINING;
case 'in_call_recording':
return CallRecordingStatus.RECORDING;
case 'transcribing':
return CallRecordingStatus.PROCESSING;
case 'completed':
return CallRecordingStatus.COMPLETED;
case 'failed':
return CallRecordingStatus.FAILED;
default:
return undefined;
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ vi.mock('src/logic-functions/data/get-current-workspace-id.util', () => ({
getCurrentWorkspaceId: getCurrentWorkspaceIdMock,
}));

vi.mock('src/logic-functions/recall-api/schedule-recall-bot.util', () => ({
scheduleRecallBot: scheduleRecallBotMock,
vi.mock('src/logic-functions/providers/schedule-call-recorder-bot.util', () => ({
scheduleCallRecorderBot: scheduleRecallBotMock,
}));

const NOW = new Date('2026-01-01T12:00:00.000Z');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@ vi.mock('src/logic-functions/data/get-current-workspace-id.util', () => ({
getCurrentWorkspaceId: getCurrentWorkspaceIdMock,
}));

vi.mock('src/logic-functions/recall-api/schedule-recall-bot.util', () => ({
scheduleRecallBot: scheduleRecallBotMock,
vi.mock('src/logic-functions/providers/schedule-call-recorder-bot.util', () => ({
scheduleCallRecorderBot: scheduleRecallBotMock,
}));

vi.mock('src/logic-functions/recall-api/reschedule-recall-bot.util', () => ({
rescheduleRecallBot: rescheduleRecallBotMock,
vi.mock('src/logic-functions/providers/reschedule-call-recorder-bot.util', () => ({
rescheduleCallRecorderBot: rescheduleRecallBotMock,
}));

vi.mock('src/logic-functions/recall-api/cancel-recall-bot.util', () => ({
cancelRecallBot: cancelRecallBotMock,
vi.mock('src/logic-functions/providers/cancel-call-recorder-bot.util', () => ({
cancelCallRecorderBot: cancelRecallBotMock,
}));

const NOW = new Date('2026-01-01T12:00:00.000Z');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { type CoreApiClient } from 'twenty-client-sdk/core';

import { CallRecordingRequestStatus } from 'src/logic-functions/constants/call-recording-request-status';
import { type CallRecordingRecord } from 'src/logic-functions/types/call-recording-record.type';
import { cancelRecallBot } from 'src/logic-functions/recall-api/cancel-recall-bot.util';
import { cancelCallRecorderBot } from 'src/logic-functions/providers/cancel-call-recorder-bot.util';
import { updateCallRecording } from 'src/logic-functions/data/update-call-recording.util';

// Intent-first: the stale-state cron finishes the Recall half when this call fails.
// Intent-first: the stale-state cron finishes the provider half when this call fails.
export const cancelCallRecordingRequest = async ({
client,
callRecording,
Expand All @@ -25,13 +25,13 @@ export const cancelCallRecordingRequest = async ({
return;
}

const cancelResult = await cancelRecallBot({
const cancelResult = await cancelCallRecorderBot({
externalBotId: callRecording.externalBotId,
});

if (!cancelResult.ok) {
console.warn(
`[call-recorder] failed to cancel Recall bot for callRecording ${callRecording.id}, leaving it for the stale-state cron: ${cancelResult.errorMessage}`,
`[call-recorder] failed to cancel provider bot for callRecording ${callRecording.id}, leaving it for the stale-state cron: ${cancelResult.errorMessage}`,
);

return;
Expand Down
Loading
Loading