Uploading calls
Formats, speaker roles, size limits, and duplicate handling.
Upload a call by sending multipart/form-data to the project's upload endpoint:
curl -X POST "$BASE/v1/projects/$PROJECT_ID/calls/upload" \
-H "X-API-Key: $AFTERTALK_KEY" \
-F "file=@call.wav;type=audio/wav"The response is 202 Accepted with a job id — processing happens in the background. See
getting results.
Supported audio
| Formats | MP3 (audio/mpeg), WAV (audio/wav, audio/x-wav), MP4/M4A (audio/mp4, audio/x-m4a), AAC (audio/aac), OGG (audio/ogg), WebM (audio/webm) |
| Max size | 10 MB per file as multipart/form-data; 50 MB with a direct upload |
An unsupported format or an oversized file returns 400. Send the right Content-Type for the
part — that's what the check uses, not the file extension. curl doesn't know audio types and sends
application/octet-stream unless you add ;type= to the file, as in the examples here.
Files over 10 MB: direct upload
Larger files go straight to storage instead of through the API, in three steps. Ask for an upload URL with the file's name, type and exact size:
SIZE=$(wc -c < call.wav | tr -d ' ')
UPLOAD=$(curl -s -X POST "$BASE/v1/projects/$PROJECT_ID/calls/upload-url" \
-H "X-API-Key: $AFTERTALK_KEY" \
-H "Content-Type: application/json" \
-d '{"filename":"call.wav","contentType":"audio/wav","sizeBytes":'"$SIZE"'}')
export UPLOAD_ID=$(echo "$UPLOAD" | jq -r '.uploadId')
export UPLOAD_URL=$(echo "$UPLOAD" | jq -r '.uploadUrl')The format, size, project access and allowance are checked here, so a file that couldn't be
processed is refused before you send it. The response holds an uploadId and an uploadUrl that
stays valid for 10 minutes. PUT the file to that URL, with no API key, and with exactly the
Content-Type and size you declared; both are part of the URL's signature, so anything else is
rejected with 403:
curl -X PUT "$UPLOAD_URL" -H "Content-Type: audio/wav" --data-binary "@call.wav"Then queue it. language, channelRoles (the JSON spelling of channel_roles) and force work as they do on the multipart upload:
curl -X POST "$BASE/v1/projects/$PROJECT_ID/calls/upload-complete" \
-H "X-API-Key: $AFTERTALK_KEY" \
-H "Content-Type: application/json" \
-d '{"uploadId":"'"$UPLOAD_ID"'","filename":"call.wav","contentType":"audio/wav","language":"pl"}'This returns 202 Accepted, and the job id is the uploadId. Completing before the PUT finished
returns 404, and completing the same upload twice returns 409. A duplicate
also returns 409, but the file is kept: send the same request with "force": true to process it
anyway without uploading it again.
Transcription language
Calls are transcribed in Polish or English. Pass language as pl or en to say which:
curl -X POST "$BASE/v1/projects/$PROJECT_ID/calls/upload" \
-H "X-API-Key: $AFTERTALK_KEY" \
-F "file=@call.wav;type=audio/wav" \
-F "language=en"Omitting the parameter falls back to the project's defaultLanguage, which is pl unless you set
it otherwise (PUT /v1/projects/{projectId} with {"defaultLanguage": "en"}, or defaultLanguage
on create). Pick the language deliberately for English audio — the wrong one degrades the
transcript, and with it every analysis derived from the transcript. A language other than pl or
en is rejected with 400; nothing is stored or queued.
An explicit language still applies per upload, so a project can hold calls in both — the project default only
applies to uploads that don't specify a language. There is no auto-detection: an unset language means the project
default rather than "work it out from the audio".
Mapping speakers to roles
For stereo recordings where each channel is one party, pass channel_roles as a JSON object mapping
channel index (0 = left, 1 = right) to a role. The transcript then attributes each turn to a named role instead of
"Speaker 1":
curl -X POST "$BASE/v1/projects/$PROJECT_ID/calls/upload" \
-H "X-API-Key: $AFTERTALK_KEY" \
-F "file=@call.wav;type=audio/wav" \
-F 'channel_roles={"0":"agent","1":"customer"}'Malformed JSON here is ignored rather than rejected — the call still processes, just without role mapping. Double-check the value if roles are missing from the result.
To reuse the same mapping across a project, configure a speaker preset and skip the parameter.
Duplicate uploads
Uploads are de-duplicated per project by the audio's content hash. Re-sending bytes that already
exist in the project returns 409 Conflict with the id of the original job:
{
"existingJobId": "8f14e45f-ea0f-4e21-9c1a-2b3c4d5e6f70",
"existingFilename": "call.wav",
"code": "DUPLICATE_FILE"
}This makes retries safe: if a request times out and you send it again, you get a 409 pointing at
the job that already exists instead of a second charge. Treat 409 as success and continue with
existingJobId:
const res = await fetch(uploadUrl, { method: 'POST', headers, body: form });
if (res.status === 409) {
const { existingJobId } = await res.json();
return existingJobId; // already uploaded — carry on
}
const { jobId } = await res.json();
return jobId;A previously failed upload doesn't block a retry — you can re-send the same audio after a
failure. To upload identical audio deliberately, add force=true, which skips the duplicate check
and creates a separate call.
Allowance exhausted
If the organization has no audio allowance left, the upload is rejected with 402 Payment Required
before the file is stored. See rate limits and billing.