Files
upstream-mirror e7da97194a
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s
snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
2026-09-17 23:28:13 +05:00

6.7 KiB

Call Recorder — Self-hosting setup

This guide is for Twenty server admins. It covers connecting Call Recorder to the recording service (Recall.ai), the environment variables it reads, and how to wire up the webhook.

If you're on Twenty Cloud, these credentials may already be configured — check with your workspace before setting them.

What you need to wire up

Call Recorder talks to Recall.ai to send bots to meetings and to receive recordings back. Two things must be configured:

  1. A Recall.ai API key (and optionally a region), so the app can schedule, update, and cancel bots.
  2. A webhook from Recall.ai back to your deployment, so the app learns when a recording is ready and can ingest it.

Transcription runs on Recall.ai's own provider by default, which needs no extra credentials. If a workspace switches CALL_RECORDER_TRANSCRIPT_PROVIDER to Gladia, add a Gladia API key in the Recall.ai dashboard (Transcription → Gladia) for every region in use, or transcripts for that workspace fail.

Server variables

Set these on the application registration after installing (Settings → 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.
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.

Calendar scheduling (CALL_RECORDER_CALENDAR_BOT_SCHEDULING_ENABLED, the "Record my calendar meetings" toggle; turning it off cancels every scheduled recording), bot behavior settings (display name, recording notice, join timing, lobby and leave timeouts), the transcription provider (CALL_RECORDER_TRANSCRIPT_PROVIDER) and the summary settings (CALL_RECORDER_SUMMARY_ENABLED, CALL_RECORDER_ADDITIONAL_SUMMARY_PROMPT) are application variables that a workspace admin tunes inside the app — not server variables.

Configuring the Recall webhook

The app exposes a server webhook route that verifies the Recall/Svix signature, advances the matching recording's lifecycle status (JOINING → RECORDING → PROCESSING, or FAILED), and — once the recording finishes — ingests the audio, video, and transcript. It never moves a status backward, so out-of-order or duplicate deliveries are safe, and it returns a non-2xx response on signature failures so Recall retries.

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

https://<your-twenty-host>/webhooks/server/9215afe6-1497-4149-a49d-e608e239bbaf

The ID is the Recall webhook logic function.

  1. In the Recall.ai dashboard, create a webhook endpoint pointing at your deployment's webhook URL, subscribed to the bot status-change, recording, and transcript events (bot.status_change, recording.done, recording.failed, transcript.done, transcript.failed). Status-change drives the lifecycle; the recording and transcript events trigger media and transcript ingestion. Subscribing to status changes alone leaves ingestion to the reconciliation backstop.
  2. Copy the endpoint's signing secret — it starts with whsec_.
  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.

Recording lifecycle

Each recording is stored as a CallRecording record and moves through a lifecycle status: SCHEDULED → JOINING → RECORDING → PROCESSING → COMPLETED, or FAILED (with a failure reason when one is available).

A recording reaches COMPLETED only once both its audio and video have been ingested. Recall produces only the artifacts requested at bot creation (mixed MP3 + MP4); if processing fails, the recording is marked FAILED.

Recall.ai retains the source media for a limited window (about seven days by default) to stay inside its free-storage window. Twenty ingests and stores the video, audio, and transcript in its own storage, so they remain available after Recall's media expires.

A periodic reconciliation job runs as a safety net, pulling the latest status from Recall and keeping recordings and bots in sync even when a real-time webhook update is missed.

Troubleshooting

Symptom Likely cause Fix
No bot joined a meeting Recording was off, the event had no conference link, it wasn't synced from a connected calendar, or RECALL_API_KEY isn't set Confirm the event is on, upcoming, has a video link, and came from a synced calendar; confirm RECALL_API_KEY is set
Recording never reaches COMPLETED A Recall webhook was missed, or only one of audio/video was produced The reconciliation job pulls the latest status from Recall within a few minutes; if it is marked FAILED, inspect the bot in the Recall dashboard
Transcript empty, or marked pending/failed Recall hasn't finished async transcription yet, or transcription failed for that call Wait for the reconciliation job to ingest the transcript; a persistent failure leaves a marker in the transcript
Webhook rejected with 500 (Invalid webhook signature, Recall keeps retrying) RECALL_WEBHOOK_SECRET doesn't match the Recall endpoint's signing secret Re-copy the whsec_… secret from the Recall webhook endpoint into the RECALL_WEBHOOK_SECRET server variable
Webhook rejected with 500 (RECALL_WEBHOOK_SECRET … not set) RECALL_WEBHOOK_SECRET is not set Set it on the application registration
Bot left almost immediately No one was admitted before the lobby / empty-meeting timeout, or everyone left Adjust the lobby / empty-meeting timeouts in the app settings (see Customize the bot in the README) if they're too aggressive
Bot joined a meeting you didn't want recorded Recording is on by default Set the event's Recording field to Off; the scheduled bot is canceled
Summary stays empty after the transcript arrives Summaries are disabled, or the summarizer run failed (for example, out of AI credits) Confirm CALL_RECORDER_SUMMARY_ENABLED isn't false and the workspace has AI credits
Summary shows "No summary available." The transcript was empty or unintelligible No action needed; this is the expected outcome for low-quality transcripts