Upload and transcribe a file
Prepare storage, transfer bytes, queue the owned object, and retrieve its transcript.
A secure three-stage file workflow
Keep large binary transfers separate from job creation. The REST API authorizes an account-owned storage target; your client uploads there and then references the completed object.
Prepare
Declare filename, size, and media type.
Transfer
PUT bytes to one or more presigned storage URLs.
Queue
Send the completed upload reference as the job source.
Retrieve
Poll the job until text and segments are ready.
From request to transcript
A predictable sequence you can reuse in server applications, automations, and internal tools.
- 01
Inspect upload.type
After prepare-upload, branch into single or multipart transfer logic.
- 02
Finish the object
A single PUT finishes directly; multipart requires an explicit completion call.
- 03
Create the transcription
Preserve the server-provided key and storage and include exact original metadata.
Make the request
Examples use the production API origin and an environment variable for the secret key.
import { createReadStream } from "node:fs";
import { open, stat } from "node:fs/promises";
import { basename } from "node:path";
const ORIGIN = "https://fast-transcriber.com";
const TOKEN = process.env.FAST_TRANSCRIBER_API_TOKEN;
const filePath = "/absolute/path/to/interview.mp3";
if (!TOKEN) throw new Error("Set FAST_TRANSCRIBER_API_TOKEN");
const media = await stat(filePath);
// Run this helper on a trusted backend. It checks every API response.
async function api(path, { method = "GET", body } = {}) {
const response = await fetch(`${ORIGIN}${path}`, {
method,
headers: {
Authorization: `Bearer ${TOKEN}`,
...(body ? { "Content-Type": "application/json" } : {})
},
body: body ? JSON.stringify(body) : undefined
});
const payload = response.status === 204 ? null : await response.json();
if (!response.ok) {
throw new Error(`${payload?.error?.code ?? response.status}: ${payload?.error?.message ?? "Request failed"}`);
}
return { data: payload?.data, location: response.headers.get("location") };
}
const { data: target } = await api("/api/v1/uploads", {
method: "POST",
body: {
filename: basename(filePath),
content_type: "audio/mpeg",
size: media.size
}
});
if (target.upload.type === "single") {
const response = await fetch(target.upload.url, {
method: target.upload.method,
headers: { ...target.upload.headers, "content-length": String(media.size) },
body: createReadStream(filePath),
duplex: "half"
});
if (!response.ok) throw new Error(`Upload failed: ${response.status}`);
} else {
const file = await open(filePath, "r");
const completed = [];
try {
for (let index = 0; index < target.upload.parts.length; index += 4) {
const batch = target.upload.parts.slice(index, index + 4);
const uploaded = await Promise.all(batch.map(async (part) => {
const position = (part.part_number - 1) * target.upload.part_size;
const length = Math.min(target.upload.part_size, media.size - position);
if (length <= 0) throw new Error(`Invalid part ${part.part_number}`);
const buffer = Buffer.allocUnsafe(length);
const { bytesRead } = await file.read(buffer, 0, length, position);
if (bytesRead !== length) throw new Error("Could not read upload part");
const response = await fetch(part.url, { method: "PUT", body: buffer });
const etag = response.headers.get("etag");
if (!response.ok || !etag) throw new Error(`Part ${part.part_number} failed`);
return { etag, part_number: part.part_number };
}));
completed.push(...uploaded);
}
await api("/api/v1/uploads/multipart", {
method: "POST",
body: { key: target.key, storage: target.storage, upload_id: target.upload.upload_id, parts: completed }
});
} catch (error) {
await api("/api/v1/uploads/multipart", {
method: "DELETE",
body: { key: target.key, storage: target.storage, upload_id: target.upload.upload_id }
}).catch(() => undefined);
throw error;
} finally {
await file.close();
}
}
const queued = await api("/api/v1/transcriptions", {
method: "POST",
body: {
upload: {
content_type: target.content_type,
filename: basename(filePath),
key: target.key,
size: media.size,
storage: target.storage
}
}
});
if (!queued.location) throw new Error("Missing Location header");
let result = queued.data;
const deadline = Date.now() + 15 * 60_000;
while (result.status === "processing" && Date.now() < deadline) {
await new Promise((resolve) => setTimeout(resolve, 2_000));
({ data: result } = await api(queued.location));
}
if (result.status === "processing") throw new Error("Polling timed out");
console.log(result);Requirements and boundaries
- Keep the bearer key away from the presigned storage request.
- Send the exact file length in bytes.
- Preserve every server-issued upload identity value.
- Start single uploads within 15 minutes; finish multipart uploads within four hours.
- For multipart transfers, use upload.part_size exactly for every part except the last.
- Abort unrecoverable multipart sessions.
Frequently asked questions
Can I POST the file body to /api/v1/uploads?+
No. That endpoint accepts metadata and returns a presigned destination for the bytes.
How do I know whether to use multipart?+
Read data.upload.type in the prepare response.
How long are upload URLs valid?+
Single-upload URLs expire after 15 minutes. Multipart part URLs expire after four hours; begin promptly and re-prepare if they expire.
Can the uploaded object be queued by another API account?+
No. Object ownership is checked before job creation.