> ## Documentation Index
> Fetch the complete documentation index at: https://docs.60db.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Analyze your first calls

> Create an analyzer, upload two recordings, and read the problems it finds, with curl

You will create an analyzer, upload two recordings, run the analysis and read the results. You need an API key (see [Authentication](https://docs.60db.ai/authentication.md)) and two audio files, `one.mp3` and `two.mp3`.

```bash theme={null}
export BASE="https://api.60db.ai/deep-analytics"
export KEY="sk_your_api_key"
```

> [!NOTE]
>
> The key must have the **Call analysis** permission. New keys have it. For a key created earlier, open Developers → API keys, edit the key and switch on **Call analysis**. Without it every call returns `403 DA_API_KEY_SCOPE_MISSING`. See [Permissions](https://docs.60db.ai/api-reference/call-analysis/overview.md#permissions).

The examples use `jq` to read IDs from responses. Every response is `{ "success": true, "data": ... }`.

**1. Create an analyzer**

```bash theme={null}
AGENT=$(curl -s -X POST "$BASE/analysis-agents" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"name":"Refund policy check","instructions":"Flag any call where the agent gives wrong refund information."}' \
  | jq -r .data.id)
```

**2. Create a batch for your files**

The CSV lists the files by `file_name`. Audio is not sent here.

```bash theme={null}
BATCH=$(curl -s -X POST "$BASE/analysis-agents/$AGENT/batches" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"source_kind":"direct_upload","csv_text":"call_id,file_name\nA-1,one.mp3\nA-2,two.mp3\n"}' \
  | jq -r .data.id)
```

The response shows `total`, `valid` and `invalid` rows.

**3. Find the row for each file**

```bash theme={null}
curl -s "$BASE/imports/batches/$BATCH/items?state=draft" \
  -H "Authorization: Bearer $KEY" | jq '.data[] | {id, file_name}'
```

Save the row `id` of `one.mp3` as `ITEM`. Repeat steps 4 to 6 for each file.

**4. Get an upload slot**

```bash theme={null}
SLOT=$(curl -s -X POST "$BASE/imports/batches/$BATCH/items/$ITEM/upload-slot" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d "{\"size_bytes\": $(wc -c < one.mp3 | tr -d ' '), \"content_type\": \"audio/mpeg\"}")
UPLOAD_ID=$(echo "$SLOT" | jq -r .data.upload_id)
UPLOAD_URL=$(echo "$SLOT" | jq -r .data.upload.url)
```

**5. Upload the file to storage**

Send every entry of `upload.fields` first, then the file as the last field, `file`. Do not send your API key to storage.

```bash theme={null}
ARGS=()
while IFS= read -r kv; do ARGS+=(--form-string "$kv"); done < <(
  echo "$SLOT" | jq -r '.data.upload.fields | to_entries[] | "\(.key)=\(.value)"')

curl -s -X POST "$UPLOAD_URL" "${ARGS[@]}" -F "file=@one.mp3;type=audio/mpeg"
```

> [!NOTE]
>
> A `2xx` reply means success. Storage answers `403` when the slot expired and `400` when the size or type does not match. The same flow in code is in the [JavaScript](#the-same-in-javascript) section.

**6. Confirm the upload**

```bash theme={null}
curl -s -X POST "$BASE/imports/batches/$BATCH/items/$ITEM/asset-receipt" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d "{\"receipt_ref\": \"$UPLOAD_ID\"}"
```

60db checks the stored file itself. Wrong type, empty or unreadable files are rejected with an `ASSET_*` code.

**7. Analyze**

The `confirm_count` must equal the number of recordings in status `ready`.

```bash theme={null}
N=$(curl -s "$BASE/analysis-agents/$AGENT/recordings?status=ready" \
  -H "Authorization: Bearer $KEY" | jq .data.total)

curl -s -X POST "$BASE/analysis-agents/$AGENT/analyze" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d "{\"confirm_count\": $N}"
```

This uses credits. The response includes `estimated_credits_usd`.

**8. Poll until done**

```bash theme={null}
curl -s "$BASE/analysis-agents/$AGENT/recordings" \
  -H "Authorization: Bearer $KEY" | jq '.data.items[] | {title, status, verdict, problem_count}'
```

Repeat every few seconds until each `status` is `done` or `failed`.

**9. Read the results**

```bash theme={null}
# Summary of problems
curl -s "$BASE/analysis-agents/$AGENT/problems" -H "Authorization: Bearer $KEY"

# Full analysis of one call: use a call_id from the recordings list
curl -s "$BASE/calls/$CALL_ID/analysis" -H "Authorization: Bearer $KEY"
```

## The same in JavaScript

```javascript theme={null}
const BASE = 'https://api.60db.ai/deep-analytics';
const headers = { Authorization: `Bearer ${process.env.SIXTYDB_API_KEY}`, 'Content-Type': 'application/json' };

async function api(method, path, body) {
  const res = await fetch(BASE + path, { method, headers, body: body && JSON.stringify(body) });
  const json = await res.json();
  if (!json.success) throw new Error(`${json.code}: ${json.message}`);
  return json.data;
}

// 1. Analyzer and batch
const analyzer = await api('POST', '/analysis-agents', {
  name: 'Refund policy check',
  instructions: 'Flag any call where the agent gives wrong refund information.',
});
const batch = await api('POST', `/analysis-agents/${analyzer.id}/batches`, {
  source_kind: 'direct_upload',
  csv_text: 'call_id,file_name\nA-1,one.mp3\n',
});

// 2. Upload one file
const rows = await fetch(`${BASE}/imports/batches/${batch.id}/items?state=draft`, { headers }).then((r) => r.json());
const row = rows.data.find((r) => r.file_name === 'one.mp3');
const file = await (await import('node:fs/promises')).readFile('one.mp3');

const slot = await api('POST', `/imports/batches/${batch.id}/items/${row.id}/upload-slot`, {
  size_bytes: file.length,
  content_type: 'audio/mpeg',
});
const form = new FormData();
for (const [k, v] of Object.entries(slot.upload.fields)) form.append(k, v);
form.append('file', new Blob([file], { type: 'audio/mpeg' }), 'one.mp3'); // file must be last
const up = await fetch(slot.upload.url, { method: 'POST', body: form }); // no Authorization header
if (!up.ok) throw new Error(`Storage upload failed: ${up.status}`);

await api('POST', `/imports/batches/${batch.id}/items/${row.id}/asset-receipt`, { receipt_ref: slot.upload_id });

// 3. Analyze, poll, read
const ready = await api('GET', `/analysis-agents/${analyzer.id}/recordings?status=ready`);
await api('POST', `/analysis-agents/${analyzer.id}/analyze`, { confirm_count: ready.total });

let items;
do {
  await new Promise((r) => setTimeout(r, 5000));
  ({ items } = await api('GET', `/analysis-agents/${analyzer.id}/recordings`));
} while (items.some((i) => !['done', 'failed', 'cancelled'].includes(i.status)));

console.log(await api('GET', `/analysis-agents/${analyzer.id}/problems`));
```

## Next steps

* Watch your AI agents or dialer numbers so new calls are analyzed automatically: [Set sources](https://docs.60db.ai/api-reference/call-analysis/set-sources.md)
* Add recordings by URL instead of upload: [Create batch](https://docs.60db.ai/api-reference/call-analysis/create-batch.md) with `csv_urls`, then [Fetch recordings](https://docs.60db.ai/api-reference/call-analysis/fetch-recordings.md)
* Improve your AI agent from a bad call: [Suggest agent fix](https://docs.60db.ai/api-reference/call-analysis/suggest-agent-fix.md)
* All limits and errors: [Call Analysis API](https://docs.60db.ai/api-reference/call-analysis/overview.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.