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âmetro | O que muda |
|---|---|
expectedBytes | Tamanho do arquivo. Enquanto o recebido não bater, o upload não vira transcrição: 400 upload_mismatch. |
sha256 | Digest 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.
POST /v1/uploadssem corpo cria o upload vazio e devolve oid.PUT /v1/uploads/{id}anexa cada chunk (corpo bruto, em ordem, até 16 MiB).POST /v1/transcriptscomuploadIdinicia 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"
doneCada 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ção | Resposta |
|---|---|
| Arquivo (ou upload) passaria de 2 GiB | 413 upload_too_large |
| Uma requisição só acima de 100 MB | 413 da borda, antes da API |
| Chunk acima de 16 MiB | 413 chunk_too_large |
| Chunk vazio | 400 chunk_empty |
| Dois chunks em paralelo | 409 upload_busy |
| Upload inexistente (ou de outra conta) | 404 upload_not_found |
| Transcrever upload sem nenhum byte | 400 validation (upload_empty) |
Recebido diferente de expectedBytes | 400 upload_mismatch |
Áudio guardado não bate com sha256 | transcrição failed (audio_mismatch), sem cobrança |
Transcrições
Crie e consulte transcrições pela API: URL ou uploadId, idioma, separação de falantes, glossário, webhook e o formato da resposta com timestamps.
Preços e limites
US$ 0,03 por hora de áudio, US$ 0,05 com separação de falantes, cobrado ao segundo. Créditos pré-pagos, sem assinatura e US$ 2 na conta nova.