Guide

Upload and transcribe a file

Prepare storage, transfer bytes, queue the owned object, and retrieve its transcript.

Built around the real API

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.

How it works

From request to transcript

A predictable sequence you can reuse in server applications, automations, and internal tools.

  1. 01

    Inspect upload.type

    After prepare-upload, branch into single or multipart transfer logic.

  2. 02

    Finish the object

    A single PUT finishes directly; multipart requires an explicit completion call.

  3. 03

    Create the transcription

    Preserve the server-provided key and storage and include exact original metadata.

Copy and adapt

Make the request

Examples use the production API origin and an environment variable for the secret key.

Implementation
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);
Keep credentials server-side
Never expose a Fast Transcriber API key in public browser JavaScript, a mobile bundle, source control, or a prompt.
Before you ship

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.
Common questions

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.

© 2026 FastTranscriberBuilt for developers who work with speech.