# How to Export AI Call Recordings to Google Cloud Storage & Amazon S3 (2026)

> Export your AI call recordings to your own cloud. Download permanent recording links from Edesy as a CSV, then bulk-upload to Google Cloud Storage (GCS) or Amazon S3 with a copy-paste script.

Source: https://edesy.in/blog/export-call-recordings-google-cloud-storage-s3-2026

If you run voice campaigns on Edesy, the call recordings belong to you — and there will come a day when you want them living in **your own cloud storage** rather than only on the platform. Maybe compliance requires a copy in your own Google Cloud bucket, maybe your data team wants to run analytics on the audio, or maybe you're simply building a long-term archive. Whatever the reason, getting your recordings out should be a 15-minute job, not an engineering project.

This guide walks through the complete workflow: export a manifest of **permanent recording links** from the dashboard, then bulk-upload every recording to **Google Cloud Storage (GCS)** — with an Amazon S3 variant at the end. No platform-side engineering required; everything here you can do yourself.

## Quick verdict (TL;DR)

- **Step 1 — Export:** On the **Campaigns** page, select your campaigns → **Export** → tick **Recordings** and **Permanent links** → download the CSV.
- **Step 2 — Read the links:** The CSV has a **`Recording URL`** column. With "Permanent links" enabled, those URLs **never expire**, so they're safe to process at your own pace.
- **Step 3 — Upload:** Run the copy-paste Python script below. It streams each recording straight from Edesy into your GCS bucket (no local disk needed) and is safe to re-run.
- **Amazon S3 user?** Same flow — swap one block in the script (shown below).
- **Cost:** Free to export. You only pay your cloud provider for storage (cents per GB/month).

## Step 1: Export a manifest of permanent recording links

Everything starts with a CSV that lists every call alongside a link to its recording. You generate this from the dashboard — no API keys, no code.

1. Sign in to your dashboard at **`voice-agent.edesy.in`**.
2. Open the **Campaigns** page and select the campaign (or campaigns) whose recordings you want. You can also narrow by date range.
3. Click **Export**.
4. In the export dialog, tick two checkboxes:
   - **Recordings** — includes a link to each call's audio.
   - **Permanent links** — upgrades those links from short-lived (7-day) URLs to **non-expiring** ones you can safely store and process on your own systems.
5. Click export. A file like `campaigns-export-2026-06-29.csv` downloads to your machine.

> **Why "Permanent links" matters:** Without it, the export uses presigned URLs that expire after 7 days — fine for a quick integration, but if your upload job stalls or you process the file weeks later, the links would be dead. The permanent option uses signed tokens that keep working as long as the recording exists, so a large migration can't fail halfway through link rot.

If the calls you want aren't part of a campaign (for example, inbound calls), the **same export option is available per agent**: open an agent → **Call Logs** → **Export**, with the identical *Recordings* + *Permanent links* checkboxes.

## Step 2: Understand the CSV

Open the CSV and you'll see one row per call. For this task, the column that matters is **`Recording URL`**. Each value looks like:

```
https://voice-agent.edesy.in/api/public/recordings/<conversation-id>?token=v1.<signature>
```

A few things worth knowing about these links:

- **They stream the audio directly.** Opening one in a browser plays (or downloads) the recording.
- **They carry no file extension.** The format (`.mp3`, `.wav`, or `.ogg`) is determined by the call, not the URL. Our script handles this automatically.
- **Add `&download=true`** to any link and the server responds with a proper filename — `recording-<conversation-id>.mp3` (or `.wav`/`.ogg`). That's the trick the script uses to preserve the right extension.

Other useful columns in the same row include `Call SID`, `Phone Number`, `Started At`, `Duration (seconds)`, and `Disposition` — handy if you want to name files by phone number or organise them by date.

## Step 3: Upload every recording to Google Cloud Storage

Here's a self-contained Python script. It reads the CSV, streams each recording **straight from Edesy into your GCS bucket** (nothing is written to local disk), preserves the correct audio extension, and **skips files already uploaded** so you can safely re-run it.

### Prerequisites

```bash
# 1. Install the two libraries
pip install requests google-cloud-storage

# 2. Authenticate to Google Cloud (one-time, opens a browser)
gcloud auth application-default login

# 3. Make sure your target bucket exists
gcloud storage buckets create gs://my-call-recordings --location=asia-south1
```

### The script (`export_to_gcs.py`)

```python
import csv
import re
import sys
import requests
from google.cloud import storage

# ---- Configure these three values ----
CSV_PATH    = "campaigns-export-2026-06-29.csv"  # the file you downloaded
GCS_BUCKET  = "my-call-recordings"               # your bucket name
GCS_PREFIX  = "edesy-recordings/"                # folder inside the bucket
# --------------------------------------

client = storage.Client()          # uses your `gcloud auth` credentials
bucket = client.bucket(GCS_BUCKET)

# Map server content types to extensions as a fallback
CONTENT_TYPE_EXT = {
    "audio/mpeg": "mp3",
    "audio/mp3":  "mp3",
    "audio/wav":  "wav",
    "audio/x-wav": "wav",
    "audio/ogg":  "ogg",
}

def filename_for(resp):
    """Recover the real filename (with extension) from the response."""
    cd = resp.headers.get("Content-Disposition", "")
    match = re.search(r'filename="?([^"]+)"?', cd)
    if match:
        return match.group(1)
    # Fallback: derive extension from the content type
    ext = CONTENT_TYPE_EXT.get(resp.headers.get("Content-Type", "").split(";")[0], "mp3")
    return f"recording.{ext}"

uploaded = skipped = failed = 0

# utf-8-sig strips the BOM the export adds for Excel compatibility
with open(CSV_PATH, newline="", encoding="utf-8-sig") as f:
    for row in csv.DictReader(f):
        url = (row.get("Recording URL") or "").strip()
        if not url:
            continue

        # Ask the server for a download response with a proper filename
        dl_url = url + ("&download=true" if "?" in url else "?download=true")

        try:
            resp = requests.get(dl_url, stream=True, timeout=180)
        except requests.RequestException as e:
            print(f"FAILED  {row.get('Call SID', '?')}: {e}")
            failed += 1
            continue

        if resp.status_code != 200:
            # 404 usually means the recording isn't ready/available for that call
            print(f"SKIP    {row.get('Call SID', '?')}: HTTP {resp.status_code}")
            skipped += 1
            continue

        blob_name = GCS_PREFIX + filename_for(resp)
        blob = bucket.blob(blob_name)

        if blob.exists():          # idempotent: never re-upload
            skipped += 1
            continue

        blob.upload_from_file(
            resp.raw,
            content_type=resp.headers.get("Content-Type", "application/octet-stream"),
        )
        uploaded += 1
        print(f"OK      gs://{GCS_BUCKET}/{blob_name}")

print(f"\nDone. uploaded={uploaded} skipped={skipped} failed={failed}")
```

### Run it

```bash
python export_to_gcs.py
```

You'll see one line per recording as it lands in your bucket, then a summary. Because the script checks `blob.exists()` before uploading, you can interrupt it and re-run it any time — it picks up exactly where it left off.

## Amazon S3 instead? Change one block

If your destination is AWS rather than Google Cloud, the only thing that changes is the upload mechanism. Install `boto3`, make sure your AWS credentials are configured (`aws configure`), and swap the GCS client and upload call:

```python
import boto3

# ---- replace the GCS config + client with: ----
S3_BUCKET = "my-call-recordings"
S3_PREFIX = "edesy-recordings/"
s3 = boto3.client("s3")            # uses your AWS credentials

# ---- replace the blob upload block with: ----
key = S3_PREFIX + filename_for(resp)

# Idempotent check
try:
    s3.head_object(Bucket=S3_BUCKET, Key=key)
    skipped += 1
    continue                       # already uploaded
except s3.exceptions.ClientError:
    pass                           # not there yet — upload it

s3.upload_fileobj(
    resp.raw,
    S3_BUCKET,
    key,
    ExtraArgs={"ContentType": resp.headers.get("Content-Type", "application/octet-stream")},
)
uploaded += 1
print(f"OK      s3://{S3_BUCKET}/{key}")
```

Everything else — reading the CSV, the permanent links, the extension handling — stays identical.

## Prefer no script? The two-command fallback

If you'd rather not run a streaming uploader, you can do it in two stages: download the recordings to a local folder, then push the whole folder with a single command.

Download each link to a local `recordings/` folder (a trimmed version of the script above writing to disk instead of the cloud), then upload the entire folder in one shot:

```bash
# Google Cloud
gcloud storage cp --recursive ./recordings gs://my-call-recordings/edesy-recordings/

# Amazon S3
aws s3 sync ./recordings s3://my-call-recordings/edesy-recordings/
```

Both `gcloud storage cp` and `aws s3 sync` are resumable and skip files that already exist, so this path is just as safe to re-run as the streaming script.

## Verify your migration

Before you consider the job done, run two quick checks:

1. **Count match.** Compare the number of rows in your CSV that have a `Recording URL` against the object count in your bucket:
   ```bash
   gcloud storage ls gs://my-call-recordings/edesy-recordings/ | wc -l
   ```
   (Remember: rows where the recording wasn't available will show as `SKIP` in the script output — that's expected, not an error.)
2. **Spot-play a file.** Download one recording from your bucket and play it to confirm the audio is intact and the format opens cleanly.

## Automating ongoing backups

This same workflow scales into a continuous backup. Two common patterns:

- **Scheduled sync:** Put the export + upload script on a nightly cron job with a rolling date filter (for example, "yesterday's calls"). Since the script skips anything already in your bucket, overlap is harmless.
- **Event-driven:** Trigger the upload from a post-call webhook so each recording lands in your bucket shortly after the call ends.

Either way, you end up with a self-maintaining mirror of your call audio in infrastructure you control.

## Frequently asked questions

### Do the permanent recording links ever expire?

No. The "Permanent links" export option generates non-expiring HMAC-signed URLs that you can store and reuse on your own systems. They are different from the default 7-day links used for short-lived integrations. As long as the recording exists on the Edesy platform, the link keeps working.

### What audio format will the recordings be in?

It depends on the telephony path of the call. Standard Twilio and Plivo calls produce MP3, Exotel produces WAV, and native audio (LiveKit) calls produce OGG (Opus). The download script in this guide reads the file's Content-Disposition header so each recording keeps its correct extension automatically — you don't have to guess.

### Can I export recordings for a specific campaign or date range only?

Yes. On the Campaigns page you can select one or more specific campaigns before exporting, and the export respects date filters. The resulting CSV contains only the calls you selected, each with its own permanent Recording URL column.

### Is there a limit on how many recordings I can export at once?

A single campaign export returns up to 200,000 call rows, and the agent-level Call Logs export returns up to 50,000 rows. For larger archives, export in batches by date range or by campaign and run the upload script per batch — it is safe to re-run because it skips files already in your bucket.

### Will I be charged extra for exporting my recordings?

No. Exporting the CSV and downloading your recordings via the permanent links is included — you own your call data. You only pay your own cloud provider (Google Cloud or AWS) for the storage you consume on their side, which is typically a few cents per GB per month.

### Can I automate this so new recordings sync to my bucket continuously?

Yes. Schedule the export + upload script on a cron job (for example, nightly) with a rolling date range, or trigger it from a post-call webhook. Because the script is idempotent — it skips recordings already present in your bucket — running it repeatedly only uploads what is new.
