Automated AI Transcription and Captions API
Use this API to submit audio for automated transcription or captions, optionally request speaker diarization, and receive the completed result at your callback URL.
Quick Start
Submit a file and provide a callback URL. The initial response returns immediately with a job ID.
curl -X POST https://api.freetranscription.ai/customerapi/jobs \
-H "X-API-Key: YOUR_API_KEY" \
-F "file=@meeting.mp3" \
-F "service_type=transcription" \
-F "callback_url=https://example.com/transcription-callback" \
-F "diarization=true" \
-F "word_timestamps=true"
Successful submission returns HTTP 202 Accepted:
{
"job_id": "api_20260521_001500_ab12cd34ef",
"status": "queued",
"queue_position": 1,
"callback_url": "https://example.com/transcription-callback",
"service_type": "transcription",
"diarization": true
}
job_id. It is the only public identifier for the request and stays stable
even if internal processing IDs change during retry or recovery.
Authentication
Generate your API key from the API page in your FreeTranscription.AI dashboard.
You can view the complete key from that page. Send it using either X-API-Key
or a bearer token.
X-API-Key: YOUR_API_KEY
Authorization: Bearer YOUR_API_KEY
Submit Audio
Use this endpoint for both direct file uploads and remote audio URL submissions.
Multipart Upload
curl -X POST https://api.freetranscription.ai/customerapi/jobs \
-H "X-API-Key: YOUR_API_KEY" \
-F "file=@meeting.mp3" \
-F "service_type=transcription" \
-F "callback_url=https://example.com/transcription-callback" \
-F "diarization=true" \
-F "word_timestamps=true"
| Field | Required | Description |
|---|---|---|
file |
Yes, unless using audio_url |
Audio or video file. Supported extensions: mp3, wav, m4a, ogg, flac, webm, mp4, avi, mkv, mov, wma, aac, opus. |
callback_url |
Yes | Your HTTPS or HTTP endpoint that will receive the final JSON callback. |
service_type |
Yes | transcription returns the transcription JSON. captions returns SRT text generated with the editor caption exporter. |
word_timestamps |
No | Boolean. Enables word-level timing when available. |
diarization |
No | Boolean. Enables speaker labels and merged speaker segments. |
min_speakers, max_speakers |
No | Optional positive integers to guide diarization speaker count. |
Remote Audio URL
Instead of uploading a file, send JSON with an audio_url.
curl -X POST https://api.freetranscription.ai/customerapi/jobs \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"audio_url": "https://example.com/audio/meeting.mp3",
"service_type": "captions",
"callback_url": "https://example.com/transcription-callback",
"diarization": true,
"word_timestamps": true
}'
Status
Use the customer job_id returned at submission time.
curl -H "X-API-Key: YOUR_API_KEY" \
https://api.freetranscription.ai/customerapi/job/api_20260521_001500_ab12cd34ef/status
last_error.
This endpoint returns the same result payload shape as the callback after the job completes.
Callbacks
When a job reaches completed or failed, the cron script sends a
POST request to your callback_url. Return any HTTP 2xx status to mark it delivered.
Non-2xx responses are retried until the configured attempt limit is reached.
Callback Headers
| Header | Description |
|---|---|
Content-Type |
application/json |
X-Customer-Job-Id |
The customer job ID returned by POST /customerapi/jobs. |
Completed Transcription Callback
{
"job_id": "api_20260521_001500_ab12cd34ef",
"status": "completed",
"result": {
"text": "Full transcript text...",
"segments": [
{
"id": 0,
"start": 0.0,
"end": 4.52,
"text": "Thanks for joining the call.",
"speaker": "SPEAKER_00"
}
],
"speaker_text": "SPEAKER_00: Thanks for joining the call."
}
}
Completed Captions Callback
{
"job_id": "api_20260521_001500_ab12cd34ef",
"status": "completed",
"result": "1\n00:00:00,000 --> 00:00:04,520\nThanks for joining the call.\n"
}
Failed Callback
{
"job_id": "api_20260521_001500_ab12cd34ef",
"status": "failed",
"result": null
}
Sample Callback Scripts
Create a callback endpoint on your server, then send its URL as callback_url. Each example receives JSON, stores the complete payload in callback-payloads, and returns a JSON acknowledgement.
<?php
header('Content-Type: application/json');
$raw = file_get_contents('php://input');
$payload = json_decode($raw, true);
$jobId = is_array($payload) ? (string)($payload['job_id'] ?? '') : '';
if ($jobId === '' || json_last_error() !== JSON_ERROR_NONE) {
http_response_code(400);
echo json_encode(['received' => false, 'error' => 'Invalid callback JSON']);
exit;
}
$jobId = preg_replace('/[^A-Za-z0-9_-]/', '_', $jobId);
$directory = __DIR__ . '/callback-payloads';
if (!is_dir($directory) && !mkdir($directory, 0770, true) && !is_dir($directory)) {
http_response_code(500);
echo json_encode(['received' => false, 'error' => 'Storage directory unavailable']);
exit;
}
$saved = file_put_contents(
$directory . '/' . $jobId . '.json',
json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES),
LOCK_EX
);
if ($saved === false) {
http_response_code(500);
echo json_encode(['received' => false, 'error' => 'Could not store callback']);
exit;
}
echo json_encode(['received' => true, 'job_id' => $jobId]);
import express from "express";
import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
const app = express();
app.use(express.json({ limit: "10mb" }));
app.post("/callback", async (req, res) => {
const jobId = String(req.body?.job_id ?? "")
.replace(/[^A-Za-z0-9_-]/g, "_");
if (!jobId) {
return res.status(400).json({ received: false, error: "Invalid callback JSON" });
}
try {
const directory = path.join(process.cwd(), "callback-payloads");
await mkdir(directory, { recursive: true });
await writeFile(
path.join(directory, `${jobId}.json`),
JSON.stringify(req.body, null, 2),
"utf8"
);
return res.json({ received: true, job_id: jobId });
} catch (error) {
return res.status(500).json({ received: false, error: "Could not store callback" });
}
});
app.listen(3000);
import json
import re
from pathlib import Path
from flask import Flask, jsonify, request
app = Flask(__name__)
@app.post("/callback")
def callback():
payload = request.get_json(silent=True)
job_id = str(payload.get("job_id", "")) if isinstance(payload, dict) else ""
job_id = re.sub(r"[^A-Za-z0-9_-]", "_", job_id)
if not job_id:
return jsonify(received=False, error="Invalid callback JSON"), 400
directory = Path("callback-payloads")
directory.mkdir(parents=True, exist_ok=True)
(directory / f"{job_id}.json").write_text(
json.dumps(payload, indent=2, ensure_ascii=False),
encoding="utf-8",
)
return jsonify(received=True, job_id=job_id)
app.run(port=3000)
package main
import (
"encoding/json"
"net/http"
"os"
"path/filepath"
"regexp"
)
var unsafeFilename = regexp.MustCompile(`[^A-Za-z0-9_-]`)
func callback(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
r.Body = http.MaxBytesReader(w, r.Body, 10<<20)
var payload map[string]any
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]any{"received": false, "error": "Invalid callback JSON"})
return
}
jobID, _ := payload["job_id"].(string)
jobID = unsafeFilename.ReplaceAllString(jobID, "_")
if jobID == "" {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]any{"received": false, "error": "Missing job_id"})
return
}
if err := os.MkdirAll("callback-payloads", 0750); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
data, err := json.MarshalIndent(payload, "", " ")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
filename := filepath.Join("callback-payloads", jobID+".json")
if err := os.WriteFile(filename, data, 0600); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(map[string]any{"received": true, "job_id": jobID})
}
func main() {
http.HandleFunc("/callback", callback)
http.ListenAndServe(":3000", nil)
}
using System.Text.Json;
using System.Text.RegularExpressions;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/callback", async (HttpRequest request) =>
{
try
{
using var document = await JsonDocument.ParseAsync(request.Body);
if (!document.RootElement.TryGetProperty("job_id", out var jobIdElement))
{
return Results.BadRequest(new { received = false, error = "Missing job_id" });
}
var jobId = Regex.Replace(jobIdElement.GetString() ?? "", "[^A-Za-z0-9_-]", "_");
if (jobId.Length == 0)
{
return Results.BadRequest(new { received = false, error = "Missing job_id" });
}
var directory = Path.Combine(AppContext.BaseDirectory, "callback-payloads");
Directory.CreateDirectory(directory);
var json = JsonSerializer.Serialize(
document.RootElement,
new JsonSerializerOptions { WriteIndented = true }
);
await File.WriteAllTextAsync(Path.Combine(directory, $"{jobId}.json"), json);
return Results.Json(new { received = true, job_id = jobId });
}
catch (JsonException)
{
return Results.BadRequest(new { received = false, error = "Invalid callback JSON" });
}
});
app.Run();
package com.example.callback;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class CallbackController {
private static final Path DIRECTORY = Path.of("callback-payloads");
private static final ObjectMapper JSON = new ObjectMapper()
.enable(SerializationFeature.INDENT_OUTPUT);
@PostMapping("/callback")
public ResponseEntity<Map<String, Object>> receive(@RequestBody JsonNode payload)
throws IOException {
if (!payload.hasNonNull("job_id")) {
return ResponseEntity.badRequest()
.body(Map.of("received", false, "error", "Missing job_id"));
}
String jobId = payload.get("job_id").asText()
.replaceAll("[^A-Za-z0-9_-]", "_");
if (jobId.isEmpty()) {
return ResponseEntity.badRequest()
.body(Map.of("received", false, "error", "Missing job_id"));
}
Files.createDirectories(DIRECTORY);
JSON.writeValue(DIRECTORY.resolve(jobId + ".json").toFile(), payload);
return ResponseEntity.ok(Map.of("received", true, "job_id", jobId));
}
}
require "sinatra"
require "json"
require "fileutils"
post "/callback" do
content_type :json
begin
payload = JSON.parse(request.body.read)
rescue JSON::ParserError
halt 400, { received: false, error: "Invalid callback JSON" }.to_json
end
job_id = payload.fetch("job_id", "").to_s.gsub(/[^A-Za-z0-9_-]/, "_")
if job_id.empty?
halt 400, { received: false, error: "Missing job_id" }.to_json
end
directory = File.join(__dir__, "callback-payloads")
FileUtils.mkdir_p(directory)
filename = File.join(directory, "#{job_id}.json")
File.open(filename, File::WRONLY | File::CREAT | File::TRUNC, 0o600) do |file|
file.write(JSON.pretty_generate(payload))
end
{ received: true, job_id: job_id }.to_json
end
Errors
| Status | Meaning |
|---|---|
400 |
Invalid request, missing or invalid service_type, missing callback URL, unsupported file type, or invalid options. |
401 |
Missing, invalid, or revoked API key. |
403 |
The customer account associated with the API key is not active. |
404 |
Unknown route or job ID. |
409 |
Result requested before the job is completed. |
413 |
Uploaded file exceeds the configured size limit. |
502 |
The job was stored, but Salad queue submission failed. |
{
"error": "callback_url must be a valid URL"
}
Reference
| Endpoint | Purpose |
|---|---|
GET /customerapi/health |
Check service availability and configured upload size. |
POST /customerapi/jobs |
Submit a multipart file upload or JSON remote audio URL for automated transcription or captions. |
GET /customerapi/job/{job_id}/status |
Fetch status, queue position, callback delivery state, and timing fields. |
GET /customerapi/job/{job_id}/result |
Fetch the final transcription JSON or captions SRT callback payload after completion. |