> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.callix.dev.br/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.callix.dev.br/_mcp/server.

# Usando a API para ler dados de uma campanha

Para exportar dados de ligações usando a API é preciso utilizar a interface do sistema para gerar um [token de acesso](/introducao/visao-geral#autenticação) da API **e fazer ligações**.

Cumprindo esses requisitos, siga os passos descritos abaixo:

## 1. Obter ligações completadas incluindo dados do contato

Um caso de uso comum é obter as ligações completadas de um dia, trazendo junto com elas os dados preenchidos dos seus contatos. Para isso é necessário usar o parâmetro `include` e a entidade `campaign_contact` como o dado a ser incluído. Isso fará com que o fim do JSON de ligações tenha um JSON aninhado com todos os dados de contato.

#### Exemplo de chamada

```bash
curl -X GET "https://sua-conta.callix.com.br/api/v1/campaign_completed_calls?filter[started_at]=2025-10-01T00:00:00.000Z,2025-10-31T23:59:59.999Z&include=campaign_contact" \
  -H "Authorization: Bearer SEU_TOKEN"
```

#### Exemplo de resposta

```json
{
  "meta": {
    "count": 1
  },
  "links": {
    "first": "/api/v1/campaign_completed_calls?filter%5Bstarted_at%5D=2021-08-30T03%3A00%3A00.000Z%2C2021-08-31T02%3A59%3A59.000Z&include=campaign_contact&page%5Blimit%5D=100&page%5Boffset%5D=0",
    "last": "/api/v1/campaign_completed_calls?filter%5Bstarted_at%5D=2021-08-30T03%3A00%3A00.000Z%2C2021-08-31T02%3A59%3A59.000Z&include=campaign_contact&page%5Blimit%5D=100&page%5Boffset%5D=0"
  },
  "data": [
    {
      "type": "campaign_completed_calls",
      "id": "82518",
      "attributes": {
        "started_at": "2021-08-31T02:23:57.013000Z",
        "ended_at": "2021-08-31T02:24:16.453000Z",
        "duration": 19,
        "destination_phone": "+5511993198168",
        "protocol": 3038,
        "note": null,
        "answered_at": "2021-08-31T02:24:08.713000Z",
        "hangup_cause": 2,
        "service_duration": 8,
        "has_audio": true,
        "audio_link": "/api/v1/campaign_completed_calls/82518/audio"
      },
      "relationships": {
        "campaign": {
          "data": {
            "type": "campaigns",
            "id": "84"
          },
          "links": {
            "self": "/api/v1/campaign_completed_calls/82518/relationships/campaign",
            "related": "/api/v1/campaign_completed_calls/82518/campaign"
          }
        },
        "campaign_list": {
          "data": {
            "type": "campaign_lists",
            "id": "168"
          },
          "links": {
            "self": "/api/v1/campaign_completed_calls/82518/relationships/campaign_list",
            "related": "/api/v1/campaign_completed_calls/82518/campaign_list"
          }
        },
        "campaign_contact": {
          "data": {
            "type": "campaign_contacts",
            "id": "663468"
          },
          "links": {
            "self": "/api/v1/campaign_completed_calls/82518/relationships/campaign_contact",
            "related": "/api/v1/campaign_completed_calls/82518/campaign_contact"
          }
        },
        "qualification": {
          "data": {
            "type": "qualifications",
            "id": "49"
          },
          "links": {
            "self": "/api/v1/campaign_completed_calls/82518/relationships/qualification",
            "related": "/api/v1/campaign_completed_calls/82518/qualification"
          }
        },
        "agent": {
          "data": {
            "type": "users",
            "id": "25"
          },
          "links": {
            "self": "/api/v1/campaign_completed_calls/82518/relationships/agent",
            "related": "/api/v1/campaign_completed_calls/82518/agent"
          }
        }
      },
      "links": {
        "self": "/api/v1/campaign_completed_calls/82518"
      }
    }
  ],
  "included": [
    {
      "type": "campaign_contacts",
      "id": "663468",
      "attributes": {
        "codigo": "1522",
        "telefone": "+5511993198168"
      },
      "links": {
        "self": "/api/v1/campaign_contacts/663468"
      }
    }
  ]
}
```

##### Formato de data

Os valores de data e hora são formatados de acordo com o padrão [ISO-8601](https://pt.wikipedia.org/wiki/ISO_8601), ou seja, `YYYY-MM-DDThh:mm:ss.sssZ`. As datas retornadas nas respostas estão convertidas no fuso horário UTC+0, indicado pela letra Z. As datas enviadas também devem seguir esse padrão.

No exemplo acima, estão sendo solicitadas todas as ligações completadas de campanha que foram feitas no mês de outubro/2025. Ou seja, ligações feitas no seguinte intervalo:

* 01/10/2025 - 00:00:00 UTC (`2025-10-01T00:00:00.000Z`)
* 31/10/2025 - 23:59:59 UTC (`2025-10-31T23:59:59.999Z`)

##### Dados do contato

Usando a chamada acima, com o parâmetro `include=campaign_contact`, os dados do cliente vem no atributo `included` da resposta:

```json startLine=83
"included": [
  {
    "type": "campaign_contacts",
    "id": "663468",
    "attributes": {
      "codigo": "1522",
      "telefone": "+5511993198168"
    },
    "links": {
      "self": "/api/v1/campaign_contacts/663468"
    }
  }
]
```

## 2. Paginação de resultados

#### Exemplo de chamada

```bash
curl -X GET "https://sua-conta.callix.com.br/api/v1/campaign_completed_calls?filter[started_at]=2025-10-01T00:00:00.000Z,2025-10-31T23:59:59.999Z&page[limit]=10&page[offset]=0" \
  -H "Authorization: Bearer SEU_TOKEN"
```

#### Exemplo de resposta

```json
{
  "meta": {
    "count": 245
  },
  "links": {
    "first": "/api/v1/campaign_completed_calls?filter%5Bstarted_at%5D=2021-01-05T03%3A00%3A00.000Z%2C2021-09-07T02%3A59%3A59.000Z&page%5Blimit%5D=4&page%5Boffset%5D=0",
    "last": "/api/v1/campaign_completed_calls?filter%5Bstarted_at%5D=2021-01-05T03%3A00%3A00.000Z%2C2021-09-07T02%3A59%3A59.000Z&page%5Blimit%5D=4&page%5Boffset%5D=244",
    "next": "/api/v1/campaign_completed_calls?filter%5Bstarted_at%5D=2021-01-05T03%3A00%3A00.000Z%2C2021-09-07T02%3A59%3A59.000Z&page%5Blimit%5D=4&page%5Boffset%5D=4"
  },
  "data": [
    {
      "type": "campaign_completed_calls",
      "id": "6153",
      "attributes": {
        "started_at": "2021-01-06T20:57:50.997000Z",
        "ended_at": "2021-01-06T21:02:14.157000Z",
        "duration": 263,
        "destination_phone": "+5537999743290",
        "protocol": 555,
        "note": null,
        "answered_at": "2021-01-06T20:57:58.377000Z",
        "hangup_cause": 1,
        "service_duration": 256,
        "has_audio": true,
        "audio_link": "/api/v1/campaign_completed_calls/6153/audio"
      },
      "relationships": {
        "campaign": {
          "data": {
            "type": "campaigns",
            "id": "6"
          },
          "links": {
            "self": "/api/v1/campaign_completed_calls/6153/relationships/campaign",
            "related": "/api/v1/campaign_completed_calls/6153/campaign"
          }
        },
        "campaign_list": {
          "data": {
            "type": "campaign_lists",
            "id": "9"
          },
          "links": {
            "self": "/api/v1/campaign_completed_calls/6153/relationships/campaign_list",
            "related": "/api/v1/campaign_completed_calls/6153/campaign_list"
          }
        },
        "campaign_contact": {
          "data": {
            "type": "campaign_contacts",
            "id": "560643"
          },
          "links": {
            "self": "/api/v1/campaign_completed_calls/6153/relationships/campaign_contact",
            "related": "/api/v1/campaign_completed_calls/6153/campaign_contact"
          }
        },
        "qualification": {
          "links": {
            "self": "/api/v1/campaign_completed_calls/6153/relationships/qualification",
            "related": "/api/v1/campaign_completed_calls/6153/qualification"
          }
        },
        "agent": {
          "data": {
            "type": "users",
            "id": "5"
          },
          "links": {
            "self": "/api/v1/campaign_completed_calls/6153/relationships/agent",
            "related": "/api/v1/campaign_completed_calls/6153/agent"
          }
        }
      },
      "links": {
        "self": "/api/v1/campaign_completed_calls/6153"
      }
    },
    {
      "type": "campaign_completed_calls",
      "id": "6169",
      "attributes": {
        "started_at": "2021-01-07T15:06:07.809000Z",
        "ended_at": "2021-01-07T15:06:35.009000Z",
        "duration": 27,
        "destination_phone": "+5511946825709",
        "protocol": 567,
        "note": null,
        "answered_at": "2021-01-07T15:06:25.069000Z",
        "hangup_cause": 2,
        "service_duration": 10,
        "has_audio": true,
        "audio_link": "/api/v1/campaign_completed_calls/6169/audio"
      },
      "relationships": {
        "campaign": {
          "data": {
            "type": "campaigns",
            "id": "6"
          },
          "links": {
            "self": "/api/v1/campaign_completed_calls/6169/relationships/campaign",
            "related": "/api/v1/campaign_completed_calls/6169/campaign"
          }
        },
        "campaign_list": {
          "data": {
            "type": "campaign_lists",
            "id": "9"
          },
          "links": {
            "self": "/api/v1/campaign_completed_calls/6169/relationships/campaign_list",
            "related": "/api/v1/campaign_completed_calls/6169/campaign_list"
          }
        },
        "campaign_contact": {
          "data": {
            "type": "campaign_contacts",
            "id": "560641"
          },
          "links": {
            "self": "/api/v1/campaign_completed_calls/6169/relationships/campaign_contact",
            "related": "/api/v1/campaign_completed_calls/6169/campaign_contact"
          }
        },
        "qualification": {
          "data": {
            "type": "qualifications",
            "id": "5"
          },
          "links": {
            "self": "/api/v1/campaign_completed_calls/6169/relationships/qualification",
            "related": "/api/v1/campaign_completed_calls/6169/qualification"
          }
        },
        "agent": {
          "data": {
            "type": "users",
            "id": "5"
          },
          "links": {
            "self": "/api/v1/campaign_completed_calls/6169/relationships/agent",
            "related": "/api/v1/campaign_completed_calls/6169/agent"
          }
        }
      },
      "links": {
        "self": "/api/v1/campaign_completed_calls/6169"
      }
    },
    {
      "type": "campaign_completed_calls",
      "id": "6180",
      "attributes": {
        "started_at": "2021-01-07T19:13:46.469000Z",
        "ended_at": "2021-01-07T19:14:00.429000Z",
        "duration": 13,
        "destination_phone": "+5514999011006",
        "protocol": 574,
        "note": null,
        "answered_at": "2021-01-07T19:13:52.789000Z",
        "hangup_cause": 1,
        "service_duration": 8,
        "has_audio": true,
        "audio_link": "/api/v1/campaign_completed_calls/6180/audio"
      },
      "relationships": {
        "campaign": {
          "data": {
            "type": "campaigns",
            "id": "6"
          },
          "links": {
            "self": "/api/v1/campaign_completed_calls/6180/relationships/campaign",
            "related": "/api/v1/campaign_completed_calls/6180/campaign"
          }
        },
        "campaign_list": {
          "data": {
            "type": "campaign_lists",
            "id": "9"
          },
          "links": {
            "self": "/api/v1/campaign_completed_calls/6180/relationships/campaign_list",
            "related": "/api/v1/campaign_completed_calls/6180/campaign_list"
          }
        },
        "campaign_contact": {
          "data": {
            "type": "campaign_contacts",
            "id": "560644"
          },
          "links": {
            "self": "/api/v1/campaign_completed_calls/6180/relationships/campaign_contact",
            "related": "/api/v1/campaign_completed_calls/6180/campaign_contact"
          }
        },
        "qualification": {
          "data": {
            "type": "qualifications",
            "id": "5"
          },
          "links": {
            "self": "/api/v1/campaign_completed_calls/6180/relationships/qualification",
            "related": "/api/v1/campaign_completed_calls/6180/qualification"
          }
        },
        "agent": {
          "data": {
            "type": "users",
            "id": "5"
          },
          "links": {
            "self": "/api/v1/campaign_completed_calls/6180/relationships/agent",
            "related": "/api/v1/campaign_completed_calls/6180/agent"
          }
        }
      },
      "links": {
        "self": "/api/v1/campaign_completed_calls/6180"
      }
    },
    {
      "type": "campaign_completed_calls",
      "id": "6181",
      "attributes": {
        "started_at": "2021-01-07T19:24:19.012000Z",
        "ended_at": "2021-01-07T19:25:46.452000Z",
        "duration": 87,
        "destination_phone": "+5582991781462",
        "protocol": 575,
        "note": null,
        "answered_at": "2021-01-07T19:24:29.152000Z",
        "hangup_cause": 1,
        "service_duration": 78,
        "has_audio": true,
        "audio_link": "/api/v1/campaign_completed_calls/6181/audio"
      },
      "relationships": {
        "campaign": {
          "data": {
            "type": "campaigns",
            "id": "6"
          },
          "links": {
            "self": "/api/v1/campaign_completed_calls/6181/relationships/campaign",
            "related": "/api/v1/campaign_completed_calls/6181/campaign"
          }
        },
        "campaign_list": {
          "data": {
            "type": "campaign_lists",
            "id": "9"
          },
          "links": {
            "self": "/api/v1/campaign_completed_calls/6181/relationships/campaign_list",
            "related": "/api/v1/campaign_completed_calls/6181/campaign_list"
          }
        },
        "campaign_contact": {
          "data": {
            "type": "campaign_contacts",
            "id": "560645"
          },
          "links": {
            "self": "/api/v1/campaign_completed_calls/6181/relationships/campaign_contact",
            "related": "/api/v1/campaign_completed_calls/6181/campaign_contact"
          }
        },
        "qualification": {
          "data": {
            "type": "qualifications",
            "id": "6"
          },
          "links": {
            "self": "/api/v1/campaign_completed_calls/6181/relationships/qualification",
            "related": "/api/v1/campaign_completed_calls/6181/qualification"
          }
        },
        "agent": {
          "data": {
            "type": "users",
            "id": "5"
          },
          "links": {
            "self": "/api/v1/campaign_completed_calls/6181/relationships/agent",
            "related": "/api/v1/campaign_completed_calls/6181/agent"
          }
        }
      },
      "links": {
        "self": "/api/v1/campaign_completed_calls/6181"
      }
    }
  ]
}
```

##### Parâmetros

Para paginar a requisição deve-se adicionar dois parâmetros, são eles:

* **`page[limit]`** que é um valor inteiro maior que 0 e representa a quantidade de
  registros a ser recuperada em cada requisição. Na ausência desse parâmetro, seu
  valor padrão é 100.
* **`page[offset]`** que é um valor inteiro maior ou igual a 0 e representa o deslocamento
  de registros. Na ausência desse parâmetro seu valor padrão é 0.

A resposta de requisição trás a **contagem do total** de registros de acordo com os filtros
aplicados (`meta.count`). Com esse valor, é possível calcular a paginação.

```jsonc startLine=2
    "meta": {
      "count": 245
    },
```

###### Exemplos

* Primeira página com paginação de tamanho 10.
  * `page[limit]=10`
  * `page[offset]=0`

* Terceira página com paginação de tamanho 100.
  * `page[limit]=100`
  * `page[offset]=200`

## 3. Ler Áudio da Chamada

Retorna o áudio da chamada em formato base64.

#### Exemplo de chamada

```bash
curl -X GET "https://sua-conta.callix.com.br/api/v1/campaign_completed_calls/:id/audio" \
  -H "Authorization: Bearer SEU_TOKEN"
```

##### Parâmetros

Para obter o áudio de uma chamada, deve-se adicionar apenas
o ID da chamada. O link obtido através do atributo audio_link na resposta das chamadas completadas já
contém o ID da chamada, então basta utilizá-lo.

A resposta dessa requisição trás o **audio da chamada** em formato **base64**. Com esse formato, é possível
criar um arquivo de áudio de extensão .mp3.

## 4. Programa de Exemplo

O script Python abaixo contém uma implementação de exemplo usando Python para consumir as APIs de obter ligações completadas e não completadas de campanha, bem como a API de download dos audios.
Ele foi construído ao redor de dois casos de uso comuns:

a) Baixar as ligações do dia anterior: agenda-se a execução do script para a madrugada (por exemplo 03:00 AM) do dia corrente, baixando as ligações do dia anterior para fazer algum relatório

b) Baixar as ligações de uma faixa de dias: deseja-se baixar um histórico de todas as ligações, por exemplo mensalmente, para fins de armazenamento local. Então por exemplo no dia primeiro de cada mês agenda-se a execução do script para baixar as ligações do mês anterior.

Para facilitar a operação, o script aceita alguns parâmetros, explicados abaixo:

uso:

```console
python export_calls_v2.py [-h] [-y] [-d] [-m] -t TENANT -b BEARER [start_date] [end_date]
```

Busca e escreve dados de chamadas de campanha usando a API Callix.

O script cria um subdiretório abaixo do diretório corrente chamado \call_audios, onde cria dois arquivos CSV, um para chamadas completadas (completed_calls.csv) e outro para chamadas não completadas (missed_calls.csv), e caso o parâmetro -d seja especificado, baixa o audio em formato mp3 de cada chamada completada.

**parâmetros posicionais**:

* start_date : Data de início no formato dd/mm/aaaa (requerido se o parâmetro -y não for usado)
* end_date : Data de fim no formato dd/mm/aaaa (requerido se o parâmetro -y não for usado)

outros parâmetros:

-h, --help            : mostra ajuda para os comandos

-y, --yesterday       : Use a data de ontem como data de início e fim, para facilitar o uso de baixar ligações do dia anterior

-d, --download        : Parâmetro que indica se é desejado fazer o download dos audios das ligações completadas (default: False)

-m, --missed          : Parâmetro que indica se é desejado fazer o download das ligações não completadas (default: False)

-t TENANT, --tenant TENANT : Qual o prefixo do tenant (Cliente) para a URL de base da API (exemplo: 'globo' para [https://sua-conta.callix.com.br](https://sua-conta.callix.com.br))

-b BEARER, --bearer BEARER : Bearer token da Autorização (crie o Token em Configurações>APIs>Acesso à API e conecte-o a um Perfil de Acesso adequado) (por exemplo, '14ae293d-f1f0-4f0c-a220-c4bc9438a855')

O programa é fornecido "as-is", como um exemplo de implementação de consumo de API da Callix.

##### Campos de campanha precisam ser adaptados no script

> **Note**
>
> Como os campos de formulário de campanha são variáveis, para exportar dados de contato com os campos que você deseja você precisa editar a função "process_included_entities()" e incluir os campos que você deseja.
> O exemplo mostra um caso com 5 campos de formulário e como mapeá-los para os campos no CSV.
> Além de alterar esta função, você também precisa alterar o array "relationship_fieldnames" que fica dentro da função fetch_and_write_calls(), este array define quais campos serão colocados no CSV.

A Callix não se propõe a dar suporte neste script e fornece-o simplesmente para dar um "ponta pé inicial" em esforços de integração com a Callix.

#### Instruções para executar o script

Para executar o script você precisa:
Ter a última versão do Python instalado. Você pode baixar a última versão navegando para [https://www.python.org/downloads/](https://www.python.org/downloads/)

Uma vez instalado o Python, instale os módulos requests e pytz usando o pip. Na linha de comando:

```console
pip install requests

pip install pytz
```

Para rodar o script copie-o para algum diretório e chame-o da linha de comando, por exemplo:

```console
python export_calls_v2.py -y -t globo -b 14ae293d-f1f0-4f0c-a220-c4bc9438a855 -d
```

(faz o download das ligações completadas, além dos seus audios. Ligações não completadas não são baixadas (falta -m))

#### Script Python de exemplo

```python
# modules that need install : run [pip install <module>] on the command line
import requests
import pytz
# modules included in Python default libraries
import json
import csv
from datetime import datetime, timedelta
import time
import base64
import os
import sys
import argparse

def convert_to_brt(utc_string):
    utc_zone = pytz.utc
    brt_zone = pytz.timezone('America/Sao_Paulo')
    utc_dt = utc_zone.localize(datetime.fromisoformat(utc_string.replace('Z', '')))
    brt_dt = utc_dt.astimezone(brt_zone)
    return brt_dt.strftime('%d-%m-%Y %H:%M:%S')

def process_included_entities(calls, included):
    
    # Create a lookup dictionary for included entities
    entity_map = {(item['type'], item['id']): item['attributes'] for item in included}
    
    # Process each call and add related entity attributes
    for call in calls:
        attrs = call['attributes']
        relationships = call['relationships']
        
        # Convert UTC times to BRT
        attrs['started_at'] = convert_to_brt(attrs['started_at'])
        attrs['ended_at'] = convert_to_brt(attrs['ended_at'])
        if 'answered_at' in attrs:
            attrs['answered_at'] = convert_to_brt(attrs['answered_at'])
        
        # Add campaign_contact attributes
        # contact attributes are DYNAMIC, so lines mapping contact attributes to CSV fields should be changed/added according to what contact fields one wants on the CSV
        # As an EXAMPLE five contact attributes are included: "nome", "cpf", "produto" , "publico", "telefone", and they are mapped to the "contact_nome", "contact_cpf" and so on as CSV fields
        contact_data = relationships.get('campaign_contact', {}).get('data')
        if contact_data and (contact_data['type'], contact_data['id']) in entity_map:
            contact_attrs = entity_map[(contact_data['type'], contact_data['id'])]
            attrs['contact_nome'] = contact_attrs.get('nome', '')
            attrs['contact_cpf'] = contact_attrs.get('cpf', '')
            attrs['contact_produto'] = contact_attrs.get('produto', '')
            attrs['contact_publico'] = contact_attrs.get('publico', '')
            attrs['contact_telefone'] = contact_attrs.get('telefone', '')

        # Add qualification attributes if they exist
        qual_data = relationships.get('qualification', {}).get('data')
        if qual_data and (qual_data['type'], qual_data['id']) in entity_map:
            qual_attrs = entity_map[(qual_data['type'], qual_data['id'])]
            attrs['qualification_name'] = qual_attrs.get('name', '')
            attrs['qualification_cpc'] = qual_attrs.get('cpc', '')
            attrs['qualification_success'] = qual_attrs.get('success', '')
            attrs['qualification_is_system_qualification'] = qual_attrs.get('is_system_qualification', '')
        
        # Add user (agent) attributes if they exist
        agent_data = relationships.get('agent', {}).get('data')
        if agent_data and (agent_data['type'], agent_data['id']) in entity_map:
            agent_attrs = entity_map[(agent_data['type'], agent_data['id'])]
            attrs['agent_name'] = agent_attrs.get('name', '')
            attrs['agent_email'] = agent_attrs.get('email', '')
            attrs['agent_login'] = agent_attrs.get('login', '')

    return calls

def download_call_audio(audio_link, protocol, headers, base_url, output_dir="call_audios"):
    if not os.path.exists(output_dir):
        os.makedirs(output_dir)
    
    filename = f"{output_dir}/{protocol}.mp3"
    
    # Check if audio file already exists locally
    if os.path.exists(filename) and os.path.getsize(filename) > 0:
        print(f"Audio for protocol {protocol} already exists and is non-empty, skipping download")
        return True
    
    full_audio_url = base_url + audio_link
    try:
        response = requests.get(full_audio_url, headers=headers)
        #sleeps for 500ms to respect API rate limit
        time.sleep(0.5)
        if response.status_code == 200:
            content_type = response.headers.get('Content-Type', '').lower()
            
            if 'application/json' in content_type:
                audio_data = response.json().get('audio', '')
                if audio_data:
                    audio_bytes = base64.b64decode(audio_data)
                    with open(filename, 'wb') as audio_file:
                        audio_file.write(audio_bytes)
                    print(f"Downloaded audio for protocol {protocol} (base64 JSON)")
                    return True
                else:
                    print(f"No audio data in JSON response for protocol {protocol}")
                    return False
            else:
                with open(filename, 'wb') as audio_file:
                    audio_file.write(response.content)
                print(f"Downloaded audio for protocol {protocol} (direct binary)")
                return True
        else:
            print(f"Failed to download audio for protocol {protocol}: Status {response.status_code}: {response.text}")
            return False
    except Exception as e:
        print(f"Error downloading audio for protocol {protocol}: {e}")
        return False

def fetch_and_write_calls(base_url, export_type, headers, start_date, end_date, download_audio=False, output_dir="call_audios"):
    if export_type not in ["completed", "missed"]:
        raise ValueError("export_type must be 'completed' or 'missed'")
    
    # Infer endpoint, include, and filename based on export_type
    if export_type == "completed":
        endpoint = "/api/v1/campaign_completed_calls"
        include = "campaign_contact,qualification,agent"
        filename = "completed_calls.csv"
        rate_limit_delay = 7  # 10/minute = 6s + 1s buffer
    else:  # export_type == "missed"
        endpoint = "/api/v1/campaign_missed_calls"
        include = "campaign_contact"
        filename = "missed_calls.csv"
        rate_limit_delay = 61  # 1/minute = 60s + 1s buffer
    
    total_calls = 0
    is_first_file = True
    csv_path = os.path.join(output_dir, filename)
    
    if not os.path.exists(output_dir):
        os.makedirs(output_dir)
    
    try:
        current_date = start_date
        while current_date <= end_date:
            day_start = current_date.replace(hour=0, minute=0, second=0, microsecond=0)
            day_end = current_date.replace(hour=23, minute=59, second=59, microsecond=999999)
            
            start_str = day_start.strftime('%Y-%m-%dT%H:%M:%S.000Z')
            end_str = day_end.strftime('%Y-%m-%dT%H:%M:%S.000Z')
            
            params = {
                "filter[started_at]": f"{start_str},{end_str}",
                "page[limit]": 2000,
                "page[offset]": 0,
                "include": include
            }
            
            print(f"Processing {export_type} calls for {current_date.strftime('%Y-%m-%d')}...")
            is_first_page = True
            current_url = endpoint
            
            while current_url:
                response = requests.get(
                    base_url + endpoint if current_url == endpoint else current_url,
                    headers=headers,
                    params=params if current_url == endpoint else None
                )
                
                if response.status_code != 200:
                    print(f"Error on {current_date.strftime('%Y-%m-%d')}: Received status code {response.status_code}: {response.text}")
                    break
                    
                data = response.json()
                calls = data['data']
                
                if not calls:
                    print(f"No {export_type} calls found for {current_date.strftime('%Y-%m-%d')}")
                    break
                    
                included = data.get('included', [])
                if included:
                    calls = process_included_entities(calls, included)
                
                if download_audio and export_type == "completed":
                    for call in calls:
                        attrs = call['attributes']
                        if attrs.get('has_audio', False) and 'audio_link' in attrs:
                            download_call_audio(attrs['audio_link'], attrs['protocol'], headers, base_url, output_dir)
                        
                original_fieldnames = list(calls[0]['attributes'].keys())
                relationship_fieldnames = (
                    ['contact_nome', 'contact_cpf', 'contact_produto', 'contact_publico', 'contact_telefone', 'qualification_name','qualification_cpc', 'qualification_success', 'qualification_is_system_qualification',  'agent_name', 'agent_login', 'agent_email']
                    if export_type == "completed"
                    else ['contact_nome', 'contact_cpf', 'contact_produto', 'contact_publico', 'contact_telefone']
                )
                all_fieldnames = original_fieldnames + [f for f in relationship_fieldnames if f not in original_fieldnames]
                
                try:
                    mode = 'w' if is_first_file and is_first_page else 'a'
                    with open(csv_path, mode, newline='', encoding='utf-8') as csvfile:
                        writer = csv.DictWriter(csvfile, fieldnames=all_fieldnames, delimiter=';')
                        
                        if is_first_page and is_first_file:
                            writer.writeheader()
                            is_first_file = False
                        
                        for call in calls:
                            writer.writerow(call['attributes'])
                except PermissionError as pe:
                    print(f"Permission denied when writing to {csv_path}: {pe}. Please ensure the file is not open elsewhere.")
                    return
                except Exception as e:
                    print(f"Error writing to {csv_path}: {e}")
                    return
                
                total_calls += len(calls)
                print(f"Fetched and wrote {len(calls)} {export_type} calls for {current_date.strftime('%Y-%m-%d')}, total so far: {total_calls}")
                
                time.sleep(rate_limit_delay)
                
                next_page = data['links'].get('next')
                current_url = base_url + next_page if next_page else None
            
            current_date += timedelta(days=1)
                
    except requests.RequestException as e:
        print(f"Error: {e}")
    
    print(f"Finished processing {export_type} calls. Total calls saved: {total_calls}")

def main():
    parser = argparse.ArgumentParser(description="Fetch and write call data from Callix API.")
    parser.add_argument('-y', '--yesterday', action='store_true', help="Use yesterday's date for start and end date")
    parser.add_argument('-d', '--download', action='store_true', help="Download audio files for completed calls (default: False)")
    parser.add_argument('-m', '--missed', action='store_true', help="Also export missed calls (default: only completed calls)")
    parser.add_argument('-t', '--tenant', required=True, help="Tenant prefix for base URL (e.g., 'globo' for https://sua-conta.callix.com.br)")
    parser.add_argument('-b', '--bearer', required=True, help="Bearer token for Authorization (e.g., '14ae293d-f1f0-4f0c-a220-c4bc9438a855')")
    parser.add_argument('start_date', nargs='?', help="Start date in dd/mm/aaaa format (required if -y not used)")
    parser.add_argument('end_date', nargs='?', help="End date in dd/mm/aaaa format (required if -y not used)")
    
    args = parser.parse_args()

    base_url = f"https://{args.tenant}.callix.com.br"
    headers = {
        'Content-Type': "application/json",
        'Authorization': f"Bearer {args.bearer}"
    }
    
    if args.yesterday:
        yesterday = datetime.now(pytz.utc) - timedelta(days=1)
        start_date = yesterday.replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=pytz.utc)
        end_date = yesterday.replace(hour=23, minute=59, second=59, microsecond=999999, tzinfo=pytz.utc)
    else:
        if not args.start_date or not args.end_date:
            print("Error: Start date and end date are required unless -y is used.")
            print("Usage: python export_calls_v2.py -t <tenant> -b <bearer> [-y] [-d] [-m] [<start_date> <end_date>]")
            print("Example: python export_calls_v2.py -t globo -b 14ae293d-f1f0-4f0c-a220-c4bc9438a855 31/03/2025 03/04/2025")
            print("         python export_calls_v2.py -t globo -b 14ae293d-f1f0-4f0c-a220-c4bc9438a855 -y -d")
            print("Dates must be in dd/mm/aaaa format.")
            sys.exit(1)
        
        try:
            start_date = datetime.strptime(args.start_date, '%d/%m/%Y').replace(tzinfo=pytz.utc)
            end_date = datetime.strptime(args.end_date, '%d/%m/%Y').replace(tzinfo=pytz.utc)
        except ValueError as e:
            print(f"Error: Invalid date format. {e}")
            print("Usage: python export_calls_v2 -t <tenant> -b <bearer> [-y] [-d] [-m] [<start_date> <end_date>]")
            print("Example: python export_calls_v2.py -t globo -b 14ae293d-f1f0-4f0c-a220-c4bc9438a855 31/03/2025 03/04/2025")
            print("         python export_calls_v2.py -t globo -b 14ae293d-f1f0-4f0c-a220-c4bc9438a855 -y -d")
            print("Dates must be in dd/mm/aaaa format.")
            sys.exit(1)
    
    print("Starting to fetch and write completed calls...")
    fetch_and_write_calls(base_url, "completed", headers, start_date, end_date, download_audio=args.download)
    
    if args.missed:
        print("Starting to fetch and write missed calls...")
        fetch_and_write_calls(base_url, "missed", headers, start_date, end_date, download_audio=False)

if __name__ == "__main__":
    main()
```