transcrevo docs

Envio de arquivos

Envie áudio local para a API: uma requisição até 100 MB, ou chunks de até 16 MiB com retomada para arquivos de até 2 GiB, e transcreva pelo uploadId.

Se o áudio não está numa URL pública, mande o arquivo para a API e transcreva pelo uploadId. Há dois caminhos, e o primeiro serve para quase todo mundo.

Caminho simples: o arquivo numa requisição

POST /v1/uploads com o arquivo no corpo bruto. Nada de JSON, nada de multipart, nenhum Content-Type obrigatório. Uma requisição leva até 100 MB, que cobre a maior parte dos áudios; acima disso, use o caminho em chunks:

UPLOAD_ID=$(curl -s -X POST https://api.transcrevo.com/v1/uploads \
  -H "Authorization: Bearer $TRANSCREVO_API_KEY" \
  --data-binary @reuniao.mp3 | jq -r .data.id)
{
	"data": {
		"id": "0f3c2d11-8a4b-4c6e-b2d9-5e7f1a9c3b42",
		"receivedBytes": 48291840,
		"expectedBytes": null,
		"sha256": null,
		"createdAt": "2026-08-24T18:00:00.000Z"
	}
}

Com esse id, crie a transcrição:

curl https://api.transcrevo.com/v1/transcripts \
  -H "Authorization: Bearer $TRANSCREVO_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{ \"uploadId\": \"$UPLOAD_ID\", \"speakers\": true }"

Daqui em diante o fluxo é o mesmo da transcrição por URL: consulte GET /v1/transcripts/{id} até concluir.

Exemplo completo (Node.js)

import { openAsBlob } from "node:fs";

const API = "https://api.transcrevo.com";
const headers = { Authorization: `Bearer ${process.env.TRANSCREVO_API_KEY}` };

const { data: upload } = await (
	await fetch(`${API}/v1/uploads`, { method: "POST", headers, body: await openAsBlob("reuniao.mp3") })
).json();

const { data: transcript } = await (
	await fetch(`${API}/v1/transcripts`, {
		method: "POST",
		headers: { ...headers, "Content-Type": "application/json" },
		body: JSON.stringify({ uploadId: upload.id }),
	})
).json();

console.log(transcript.id, transcript.status);

Declare o arquivo: expectedBytes e sha256

Os dois são opcionais, vão na query da criação do upload, e existem para o mesmo fim: fechar a janela em que um envio incompleto vira transcrito.

BYTES=$(stat -c %s reuniao.mp3)
SHA=$(sha256sum reuniao.mp3 | cut -d" " -f1)

curl -X POST "https://api.transcrevo.com/v1/uploads?expectedBytes=$BYTES&sha256=$SHA" \
  -H "Authorization: Bearer $TRANSCREVO_API_KEY" \
  --data-binary @reuniao.mp3
ParâmetroO que muda
expectedBytesTamanho do arquivo. Enquanto o recebido não bater, o upload não vira transcrição: 400 upload_mismatch.
sha256Digest do arquivo, em hexadecimal minúsculo. Conferido contra o áudio guardado antes do processamento.

Sem expectedBytes, um chunk perdido no meio de 2 GiB é aceito em silêncio: a transcrição sai done, com texto plausível, cobrada, cobrindo só o pedaço que chegou, e não há como saber sem contar os bytes por fora. Com ele, o mesmo caso é um erro explícito na hora de transcrever.

O sha256 fecha o outro lado (arquivo trocado ou corrompido) e é conferido no despacho, onde os bytes já passam em claro: não bateu, a transcrição termina em failed com error.code audio_mismatch, sem cobrar. Ele também é o que identifica o conteúdo para o reuseIfIdentical, que evita pagar duas vezes pelo mesmo áudio.

Caminho retomável: arquivo grande em chunks

Arquivo acima de 100 MB, ou link que pode cair? Envie em pedaços (até 2 GiB no total) e retome de onde parou.

  1. POST /v1/uploads sem corpo cria o upload vazio e devolve o id.
  2. PUT /v1/uploads/{id} anexa cada chunk (corpo bruto, em ordem, até 16 MiB).
  3. POST /v1/transcripts com uploadId inicia a transcrição.
UPLOAD_ID=$(curl -s -X POST https://api.transcrevo.com/v1/uploads \
  -H "Authorization: Bearer $TRANSCREVO_API_KEY" | jq -r .data.id)

split -b 8m reuniao.mp3 chunk_

for f in chunk_*; do
  curl -X PUT "https://api.transcrevo.com/v1/uploads/$UPLOAD_ID" \
    -H "Authorization: Bearer $TRANSCREVO_API_KEY" \
    --data-binary @"$f"
done

Cada resposta devolve o total recebido até ali:

{ "data": { "id": "0f3c2d11-…", "receivedBytes": 16777216, "expectedBytes": 109051904, "sha256": "9f86d0…", "createdAt": "…" } }

Declarando expectedBytes na criação, um chunk repetido ou fora de ordem, que faria o total passar do tamanho do arquivo, é recusado com 400 upload_mismatch em vez de virar uma colagem.

Os chunks devem ir em sequência, um de cada vez: o servidor anexa cada um ao fim do arquivo. Envios em paralelo no mesmo upload são recusados com 409 upload_busy.

Retomar um upload interrompido

Caiu a conexão? Pergunte quantos bytes o servidor já recebeu e continue do ponto certo:

curl "https://api.transcrevo.com/v1/uploads/$UPLOAD_ID" \
  -H "Authorization: Bearer $TRANSCREVO_API_KEY"
{ "data": { "id": "0f3c2d11-…", "receivedBytes": 25165824, "expectedBytes": 109051904, "sha256": "9f86d0…", "createdAt": "…" } }

Retome o envio a partir do byte receivedBytes do arquivo original.

Exemplo completo (Node.js)

O laço abaixo é o caminho inteiro: declara o arquivo, manda em pedaços, e a cada falha pergunta quanto chegou e continua dali. É onde erra quem tem pressa, então vale copiar como está.

import { createHash } from "node:crypto";
import { open, stat } from "node:fs/promises";

const API = "https://api.transcrevo.com";
const headers = { Authorization: `Bearer ${process.env.TRANSCREVO_API_KEY}` };
const CHUNK = 8 * 1024 * 1024;
const path = "reuniao.mp3";

const { size } = await stat(path);
const file = await open(path);

const hash = createHash("sha256");
for await (const chunk of file.createReadStream({ autoClose: false })) hash.update(chunk);
const sha256 = hash.digest("hex");

const created = await fetch(`${API}/v1/uploads?expectedBytes=${size}&sha256=${sha256}`, { method: "POST", headers });
const { data: upload } = await created.json();

let sent = 0;
while (sent < size) {
	const buffer = Buffer.alloc(Math.min(CHUNK, size - sent));
	await file.read(buffer, 0, buffer.length, sent);
	try {
		const res = await fetch(`${API}/v1/uploads/${upload.id}`, { method: "PUT", headers, body: buffer });
		if (!res.ok) throw new Error((await res.json()).error.code);
		// A resposta é a fonte de onde continuar; não confie no seu contador.
		({ data: { receivedBytes: sent } } = await res.json());
	} catch (error) {
		// Caiu no meio: pergunte quanto chegou de verdade e retome dali.
		const state = await fetch(`${API}/v1/uploads/${upload.id}`, { headers });
		if (!state.ok) throw error;
		({ data: { receivedBytes: sent } } = await state.json());
	}
}

await file.close();

const res = await fetch(`${API}/v1/transcripts`, {
	method: "POST",
	// A chave de idempotência é o próprio uploadId: ele identifica ESTE corpo.
	// Chavear pelo id da sua mídia daria 409 em todo retry, porque cada tentativa
	// sobe o arquivo de novo e manda um uploadId diferente.
	headers: { ...headers, "Content-Type": "application/json", "Idempotency-Key": upload.id },
	body: JSON.stringify({ uploadId: upload.id, speakers: true, reuseIfIdentical: true }),
});
const { data: transcript } = await res.json();
console.log(transcript.id, transcript.status);

Limites e erros

SituaçãoResposta
Arquivo (ou upload) passaria de 2 GiB413 upload_too_large
Uma requisição só acima de 100 MB413 da borda, antes da API
Chunk acima de 16 MiB413 chunk_too_large
Chunk vazio400 chunk_empty
Dois chunks em paralelo409 upload_busy
Upload inexistente (ou de outra conta)404 upload_not_found
Transcrever upload sem nenhum byte400 validation (upload_empty)
Recebido diferente de expectedBytes400 upload_mismatch
Áudio guardado não bate com sha256transcrição failed (audio_mismatch), sem cobrança

On this page