Uploads
Three ways to bring recordings in. All of them land a call in the active project and enqueue it for analysis.
Single upload
POST /api/uploads — multipart/form-data
| Part | Required | Notes |
|---|---|---|
file | ✓ | The audio recording |
direction | — | inbound or outbound. Defaults to outbound |
customer_phone | — | Used to attach or create the customer |
customer_name | — | |
customer_segment | — | Segment name, created on demand |
curl -X POST https://your-host/api/uploads \
-H "Authorization: Bearer <token>" \
-F "[email protected]" \
-F "direction=outbound" \
-F "customer_phone=+918888888888" \
-F "customer_name=Priya Kumar"
Returns the created call, in status pending. Analysis runs in the background
— poll GET /api/calls/{id} or watch the list.
| Status | Cause |
|---|---|
400 | {"detail": "file is required"} |
Bulk upload
POST /api/uploads/bulk — multipart/form-data
Repeat the files part once per recording:
curl -X POST https://your-host/api/uploads/bulk \
-H "Authorization: Bearer <token>" \
-F "[email protected]" \
-F "[email protected]" \
-F "[email protected]"
Each file is reported on independently:
{
"results": [
{ "filename": "call-1.mp3", "call_id": 1042 },
{ "filename": "call-2.mp3", "call_id": 1043 },
{ "filename": "call-3.mp3", "error": "unsupported audio format" }
]
}
One bad file does not fail the batch. There is no customer metadata on this route — use CSV upload when you need it.
CSV upload
POST /api/uploads/csv — multipart/form-data
The richest option: a manifest of calls with their metadata, plus either the audio files alongside it or URLs to fetch them from.
| Part | Required | Notes |
|---|---|---|
csv_file | ✓ | The manifest |
audio_files | — | Recordings, matched to rows by filename |
CSV columns
The header row is matched case-insensitively. Every column is optional except
that each row needs either filename or recording_url.
| Column | Notes |
|---|---|
filename | Must match an uploaded audio_files part exactly |
recording_url | Downloaded server-side if no matching file was uploaded |
customer_name | |
customer_phone | |
customer_segment | |
segments | Comma-separated segment names, created on demand |
direction | inbound or outbound. Defaults to outbound |
filename,customer_name,customer_phone,direction,segments
call-1.mp3,Priya Kumar,+918888888888,outbound,"winback,north"
call-2.mp3,Arjun Rao,+919999999999,inbound,south
curl -X POST https://your-host/api/uploads/csv \
-H "Authorization: Bearer <token>" \
-F "[email protected]" \
-F "[email protected]" \
-F "[email protected]"
Response
{
"results": [
{ "row_index": 0, "filename": "call-1.mp3", "call_id": 1042 },
{ "row_index": 1, "filename": "call-2.mp3", "call_id": 1043, "segment_error": "failed to attach segments" },
{ "row_index": 2, "filename": "call-3.mp3", "error": "no audio file uploaded matching \"call-3.mp3\" and no recording_url provided" }
]
}
row_index is the position in the CSV, excluding the header.
Note the distinction between error and segment_error: a segment that could
not be attached does not discard a successfully ingested call. The call is
reported with its call_id so the row can be re-tagged later.
Rows with neither filename nor recording_url are skipped silently.
| Status | Cause |
|---|---|
400 | {"detail": "multipart form required"} |
400 | {"detail": "csv_file is required"} |
400 | {"detail": "failed to parse csv: …"} |
400 | {"detail": "csv must have a 'filename' or 'recording_url' column"} |
Which to use
| Situation | Route |
|---|---|
| One call, with metadata | POST /api/uploads |
| A folder of recordings, metadata not needed | POST /api/uploads/bulk |
| An export from another system | POST /api/uploads/csv |
| Recordings already hosted somewhere | POST /api/uploads/csv with recording_url |
| A server-to-server integration | Developer API |
After upload
Each call is queued through River, a Postgres-backed job queue, so a restart
mid-analysis resumes rather than losing the work. Track progress through the
call's status — see Calls.
Customers are matched on phone number and created if unknown, so a customer's history accumulates across uploads without any linking step.