transcrevo docs

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.

Criar uma transcrição

POST /v1/transcripts
CampoTipoDescrição
urlstringURL pública (http/https) do áudio.
uploadIdstringID de um arquivo enviado. Informe url ou uploadId, nunca os dois.
languagestringOpcional. Código do idioma (ex.: pt, en). Omitido: detecta sozinho.
speakersboolean, integer, rangeOpcional. Separa os falantes. Muda a tarifa.
webhookUrlstringOpcional. URL http(s) que recebe um POST quando a transcrição termina.
glossaryobjetoOpcional. Correções que valem só para este áudio (nomes próprios, jargão).
reuseIfIdenticalbooleanOpcional. Devolve a transcrição que a conta já tem deste mesmo áudio, sem cobrar de novo.

O campo speakers

Um campo só decide tudo sobre falantes:

ValorSignificado
omitidoTexto corrido, sem separar falantes.
trueSepara os falantes; a quantidade quem descobre é a API.
3São exatamente 3 falantes.
"2-5"São entre 2 e 5 falantes.

Só informe a quantidade quando souber: um palpite errado força a separação para a contagem errada. Com speakers, cada item de words traz o rótulo do falante ("A", "B", …) e a resposta traz speakerCount.

curl https://api.transcrevo.com/v1/transcripts \
  -H "Authorization: Bearer $TRANSCREVO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/reuniao.mp3",
    "speakers": 5
  }'

Resposta 201:

{
	"data": {
		"id": "6b9f2b81-1c9a-4f4e-9d5f-8f2a7c1e3b90",
		"status": "processing",
		"text": null,
		"words": null,
		"error": null,
		"language": null,
		"durationSeconds": 0,
		"speakerCount": 0,
		"cost": 0,
		"currency": "USD",
		"model": "trv-1",
		"url": "https://example.com/reuniao.mp3",
		"createdAt": "2026-08-24T18:00:00.000Z"
	}
}

Formatos aceitos: mp3, wav, m4a/aac, ogg, opus, flac, webm, wma, amr e a trilha de áudio de arquivos de vídeo (mp4, mov, mkv). O que não decodifica volta como failed com audio_invalid, sem cobrança. Duração máxima: 10 horas por áudio.

Como fica quando conclui

O mesmo objeto, agora com text, words, durationSeconds e cost preenchidos. start e end são segundos, em número decimal (1.5 é um segundo e meio, não 1500), contados do começo do áudio:

{
	"data": {
		"id": "6b9f2b81-1c9a-4f4e-9d5f-8f2a7c1e3b90",
		"status": "done",
		"text": "Bom dia a todos. Vamos começar.",
		"words": [
			{ "start": 0.32, "end": 0.78, "text": "Bom", "speaker": "A" },
			{ "start": 0.78, "end": 1.04, "text": "dia", "speaker": "A" },
			{ "start": 1.04, "end": 1.51, "text": "a", "speaker": "A" },
			{ "start": 1.51, "end": 2.06, "text": "todos.", "speaker": "A" },
			{ "start": 2.44, "end": 2.9, "text": "Vamos", "speaker": "B" },
			{ "start": 2.9, "end": 3.37, "text": "começar.", "speaker": "B" }
		],
		"error": null,
		"language": "pt",
		"durationSeconds": 3,
		"speakerCount": 2,
		"cost": 0.0042,
		"currency": "USD",
		"model": "trv-1",
		"url": "https://example.com/reuniao.mp3",
		"createdAt": "2026-08-24T18:00:00.000Z"
	}
}

text é a junção das palavras de words, e words é o que serve para legenda, busca por trecho e corte de vídeo.

A saída não é reproduzível

Mandar o mesmo arquivo duas vezes pode devolver transcritos ligeiramente diferentes: um "então mostra" vira "um monstro", a segmentação muda em algumas dezenas de blocos, a normalização de fala informal ("pra" e "para") oscila. Não é bug e não tem seed: a transcrição não é uma função determinística do arquivo.

Na prática: não use o transcrito como base de comparação bit a bit entre execuções. Para auditar uma reclamação, guarde o transcrito que você entregou (o id dele é permanente) em vez de gerar de novo.

Aviso de conclusão (webhook)

Um VOD de duas horas leva minutos para transcrever. Perguntar de três em três segundos são ~100 requisições por job só para ouvir processing. Mande webhookUrl e a API avisa uma vez, quando termina:

curl https://api.transcrevo.com/v1/transcripts \
  -H "Authorization: Bearer $TRANSCREVO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "uploadId": "0f3c2d11-8a4b-4c6e-b2d9-5e7f1a9c3b42",
    "webhookUrl": "https://seu-servidor.com/hooks/transcrevo"
  }'

O aviso é um POST com o resumo da transcrição, no mesmo envelope de sempre, e vale tanto para done quanto para failed:

{
	"data": {
		"id": "6b9f2b81-…",
		"status": "done",
		"durationSeconds": 8700,
		"language": "pt",
		"speakerCount": 6,
		"cost": 12.08,
		"currency": "USD",
		"error": null,
		"model": "trv-1",
		"url": null,
		"createdAt": "2026-08-27T18:00:00.000Z"
	}
}

text e words não vão no corpo: num áudio de duas horas eles passam de 10 MB. Recebido o aviso, busque o transcrito uma vez em GET /v1/transcripts/{id}.

Conferir a assinatura

Cada entrega leva o header Transcrevo-Signature, no formato t=<unix>,v1=<hex>. O v1 é o HMAC-SHA256 de <t>.<corpo cru>, e o segredo é o de webhook da sua conta (whsec_...), mostrado em /dashboard/webhooks.

Ele é independente das chaves de API de propósito: a chave autentica as suas requisições e é trocada quando vaza; o segredo verifica assinatura e é trocado quando o seu verificador muda. Amarrados, rotacionar a chave derrubava a verificação do seu endpoint no mesmo instante.

Ao rotacionar, o segredo antigo continua verificando por 24 horas: publique o novo, confirme, e o antigo vence sozinho. Nessa janela, aceite qualquer um dos dois.

import { createHmac, timingSafeEqual } from "node:crypto";

const secret = process.env.TRANSCREVO_WEBHOOK_SECRET;

export function valid(header, rawBody) {
	const { t, v1 } = Object.fromEntries(header.split(",").map((part) => part.split("=")));
	// Aviso velho é aviso repetido: fora de cinco minutos, recuse.
	if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
	const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
	return timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}

Responda 2xx para confirmar. Qualquer outra coisa (ou nenhuma resposta em 10s) é nova tentativa, com espera crescente: 30s, 2min, 10min, 30min, 2h e 6h. Depois da sexta a API desiste do aviso; a transcrição continua lá para ser buscada.

Glossário por requisição

Nome próprio e jargão são onde todo motor de ASR erra, e você sabe de antemão quais vão aparecer no seu áudio. glossary é { "o que ele ouve": "o que escrever" }, aplicado depois da transcrição e só naquele áudio:

curl https://api.transcrevo.com/v1/transcripts \
  -H "Authorization: Bearer $TRANSCREVO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/podcast.mp3",
    "glossary": { "gatu": "Gatto", "bertual": "Bertuol" }
  }'

Até 100 entradas, de até 80 caracteres cada. A correção vale por sequência de palavras, então "mote gruto": "Mateus Gruto" também funciona, e o intervalo de tempo é repartido entre as palavras trocadas. O que você manda tem preferência sobre as correções que já vêm de casa.

Não pagar duas vezes pelo mesmo áudio

Orquestrador que redistribui fila manda o mesmo vídeo mais de uma vez, e cada envio é uma transcrição cobrada. Para desligar isso do seu lado sem manter controle próprio, envie o arquivo com sha256 e crie a transcrição com reuseIfIdentical:

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

Existindo uma transcrição desta conta para aquele conteúdo, com os mesmos parâmetros, a resposta é 200 com ela (em vez de 201 com uma nova) e nada é cobrado. A busca alcança inclusive a que ainda está em processing, que é o caso da rajada. Sem sha256 no upload não há como reconhecer o conteúdo, e o pedido é recusado com 400 validation (reuse_needs_sha256) em vez de virar cobrança silenciosa.

Duas requisições exatamente simultâneas ainda podem criar duas transcrições: o reaproveitamento olha o que já existe, e nesse instante nenhuma das duas existe.

Repetir com segurança

Se a conexão cair depois de a requisição chegar, repetir criaria uma segunda transcrição (e uma segunda cobrança). Para evitar isso, mande um header Idempotency-Key com qualquer string de até 64 caracteres:

curl https://api.transcrevo.com/v1/transcripts \
  -H "Authorization: Bearer $TRANSCREVO_API_KEY" \
  -H "Idempotency-Key: reuniao-2026-08-25-01" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/reuniao.mp3" }'

A mesma chave devolve a mesma transcrição, quantas vezes você repetir. Usar a mesma chave com um corpo diferente devolve 409 idempotency_conflict: é sinal de que a chave foi reaproveitada por engano.

Se o áudio veio de um upload, use o próprio uploadId como chave. O instinto é chavear pelo id da sua mídia, para que repetir o trabalho inteiro não pague duas vezes, mas cada tentativa sobe o arquivo de novo, então o uploadId do corpo muda a cada uma. Mesma chave, corpo diferente: 409 idempotency_conflict em toda tentativa, para sempre. Quem resolve "mandei o mesmo vídeo duas vezes" é reuseIfIdentical, acima, não a chave de idempotência.

Consultar uma transcrição

GET /v1/transcripts/{id}
curl https://api.transcrevo.com/v1/transcripts/6b9f2b81-1c9a-4f4e-9d5f-8f2a7c1e3b90 \
  -H "Authorization: Bearer $TRANSCREVO_API_KEY"

Consulte periodicamente (a cada poucos segundos) até status sair de processing. Se o volume é grande, prefira webhookUrl na criação: é uma leitura por job em vez de uma centena.

Listar transcrições

GET /v1/transcripts
ParâmetroDescrição
pagePágina, começando em 1. Padrão: 1.
perPageItens por página, até 100. Padrão: 20.
statusOpcional. Filtra por processing, done ou failed.
sinceOpcional. Só o que foi criado a partir desta data (ISO 8601).
untilOpcional. Só o que foi criado antes desta data (ISO 8601).
curl "https://api.transcrevo.com/v1/transcripts?status=done&perPage=50" \
  -H "Authorization: Bearer $TRANSCREVO_API_KEY"

O recorte por data é feito no banco, então fechar o custo de um dia não exige paginar a conta inteira:

curl "https://api.transcrevo.com/v1/transcripts?since=2026-08-27T00:00:00Z&until=2026-08-28T00:00:00Z" \
  -H "Authorization: Bearer $TRANSCREVO_API_KEY"

Da mais recente para a mais antiga. Os itens da lista trazem todos os campos exceto text e words, que num áudio longo passam de 10 MB por item; consulte a transcrição individual para obtê-los.

{
	"data": {
		"transcripts": [{ "id": "6b9f2b81-…", "status": "done", "durationSeconds": 1834 }],
		"total": 128,
		"page": 1,
		"perPage": 20
	}
}

Apagar uma transcrição

DELETE /v1/transcripts/{id}

Apaga o texto, as palavras e o áudio guardado. O registro de uso continua existindo, porque é dele que sai a fatura do período, mas o conteúdo some e não tem como recuperar.

Transcrição ainda em processing não pode ser apagada (409 transcript_processing): o resultado chegaria depois e repovoaria o que você acabou de apagar. Espere terminar.

Campos da resposta

CampoDescrição
idIdentificador da transcrição.
statusprocessing, done ou failed.
textO texto transcrito. null até concluir.
wordsCada palavra com start, end (em segundos, decimais), text e speaker. null até concluir.
error{ code, message } quando status é failed; null fora disso.
languageIdioma detectado (ou o informado). null enquanto processa.
durationSecondsDuração real do áudio. 0 enquanto processa.
speakerCountQuantos falantes distintos foram encontrados. null quando não se pediu speakers (que é diferente de 0: pedi e não achei ninguém).
costCusto debitado, na menor unidade de currency (centavos), com fração: 0.0167 é um sexto de centavo. 0 enquanto processa e sempre 0 em falha.
currencyMoeda de cost, em ISO 4217 (USD).
modelModelo que produziu o transcrito.
urlA URL que você mandou. null quando o áudio veio de um arquivo enviado.
createdAtData de criação (ISO 8601).

Status

StatusSignificado
processingNa fila ou em processamento.
doneConcluída: text disponível e custo debitado.
failedFalhou: veja error. Nada é cobrado em falhas.

On this page