Build your own reports

Export Aircall's analytics reports and pipe them into your own dashboards or data warehouse.

The Analytics API gives you Aircall's reporting data in bulk, as CSV. It's built for historical, reporting-grade data — feeding a BI dashboard, backfilling a warehouse, or powering analytics inside your own product. For real-time call data, reach for webhooks and Conversation Intelligence instead.

Exports run asynchronously. You submit a report request, Aircall generates the report in the background, and you collect the finished CSV from a presigned, time-limited URL.

There are two ways to learn a job is done:

Webhook (Recommended)

Subscribe to analytics.report_created and analytics.report_failed. When a job finishes, the payload carries the download URL directly — no retrieve call needed.

Polling

Poll the retrieve endpoint on an interval until the job reports done, then read the download URL from the response. Back off between requests to stay within the rate limit.

Prerequisites

  • An Aircall account with the Analytics+
  • A registered app with a valid OAuth access token. See OAuth authentication
  • A publicly accessible HTTPS endpoint and a webhook subscribed to the analytics events. See Webhooks

Create an export

Send a POST with the report type and a date window. The whole payload is wrapped in an input object, and the filters live under input.filter. Only filter.timezone and filter.dateFilter are mandatory — every other filter is optional and falls back to the report's defaults. The response returns a job you'll track until the report is ready, so store its exportID.

import 'dotenv/config';

const BASE = 'https://api.aircall.io/v1';

const getAuth = () => `Bearer ${process.env.AIRCALL_ACCESS_TOKEN}`;

const createReportExport = async () => {
  const res = await fetch(`${BASE}/analytics/report/export`, {
    method: 'POST',
    headers: {
      Authorization: getAuth(),
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      input: {
        reportName: 'CALLS_HISTORY',
        filter: {
          timezone: 'Europe/Paris', // mandatory — IANA timezone
          dateFilter: { absoluteRange: { fromDate: '2026-06-01', toDate: '2026-06-30' } },
          teamIDs: [12345], // from GET /v1/teams
          // ...
        }
      }
    })
  });

  if (!res.ok) {
    const { error, troubleshoot } = await res.json().catch(() => ({}));
    throw new Error(`Export request failed (${res.status}): ${troubleshoot || error}`);
  }

  const data = await res.json();
  // Store data.exportID — used to poll, or to match an incoming webhook.
  return data;
};
📘

About the filters. Only the timezone and date_range are required; every other filter is optional and falls back to its default when omitted. The available filters differ by report type — find the full list in the Analytics API reference.

Get reports via Webhook

Subscribe to analytics.report_created and analytics.report_failed. When a job finishes, the payload carries the download URL directly — no retrieve call needed.

import express from 'express';
import { downloadReport } from './download-report.js';

const app = express();
app.use(express.json());

app.post('/aircall/webhook', (req, res) => {
  res.sendStatus(200);

  const { event, data } = req.body;

  switch (event) {
    case 'analytics.report_created':
      // The payload carries the download URL — no retrieve call needed.
      downloadReport(data.download_url).catch(console.error);
      break;
    case 'analytics.report_failed':
      console.error(`Export ${data.id} failed: ${data.error_code} — ${data.error_message}`);
      break;
  }
});

app.listen(3000);

Get reports via Polling

Poll the export until it reports done, then read the download URL from the response. There's no strict rate limit, but checking about once a minute is the recommended cadence.

const getReportExport = async (jobId) => {
  const res = await fetch(`${BASE}/analytics/report/export/${jobId}`, {
    headers: { Authorization: getAuth() }
  });
  if (!res.ok) throw new Error(`Poll failed: ${res.status}`);
  return res.json();
};

const waitForReport = async (jobId, { interval = 60000, timeout = 1800000 } = {}) => {
  const deadline = Date.now() + timeout;

  while (Date.now() < deadline) {
    const job = await getReportExport(jobId);

    if (job.status === 'COMPLETED') return job.downloadUrl;
    if (job.status === 'FAILED') throw new Error(`Export ${jobId} failed: ${job.errorMessage}`);

    await new Promise((resolve) => setTimeout(resolve, interval));
  }

  throw new Error(`Timed out waiting for export ${jobId}`);
};

Download reports CSV

The download URL is a presigned S3 link that expires one hour after it's issued, so fetch it promptly.

import { createWriteStream } from 'node:fs';
import { Readable } from 'node:stream';
import { pipeline } from 'node:stream/promises';

const downloadReport = async (url, dest = './report.csv') => {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`Download failed: ${res.status}`);

  await pipeline(Readable.fromWeb(res.body), createWriteStream(dest));
  return dest;
};

export { downloadReport };

From here, parse the CSV with whatever tooling fits your stack and load the rows into your dashboard or warehouse. The column set is fixed per report type, so you can map columns once and reuse the mapping across every export of that type. Exports have no row cap, so a wide date range can produce a very large file — stream it rather than opening it in a tool that limits imported rows.

Response codes

Status codeWhat it means
200Request accepted.
403The account lacks the Analytics+ entitlement, or the token is invalid.
500Unexpected server error

See the full list of response codes here.


Did this page help you?