Webhooks
Quando uma transcrição em lote termina, mandamos um POST para a sua webhook_url, assinado com o segredo da organização. Assim você não precisa ficar consultando o job.
Eventos
transcription.completed: o job concluiu;data.resulttraz o texto e os trechos.transcription.failed: o job falhou;data.errortraz ocodee amessage. Nada foi cobrado.
O corpo é um JSON compacto com o type e o job inteiro em data, na mesma forma de GET /v1/stt/transcriptions/{id}:
{"type": "transcription.completed","data": {"id": "0f8e2a6c-3b1d-4c9e-8a7f-5d2b1e9c4a60","status": "completed","source": "api","input": "upload","file_name": "consulta.m4a","size_bytes": 24117248,"duration_ms": 2538000,"keyterms": ["Dra. Helena Prado","losartana"],"webhook_url": "https://api.suaempresa.com.br/int8","cost_brl": 0.07755011,"error": null,"result": {"text": "bom dia dona ana paula tudo bem com a senhora trouxe sim doutora o de sangue e o eletrocardiograma","segments": [{"start_ms": 0,"end_ms": 14120,"text": "bom dia dona ana paula tudo bem com a senhora"},{"start_ms": 14120,"end_ms": 31480,"text": "trouxe sim doutora o de sangue e o eletrocardiograma"}],"duration_ms": 2538000},"created_at": "2026-10-13T14:30:02.118Z","started_at": "2026-10-13T14:30:02.540Z","completed_at": "2026-10-13T14:31:10.902Z","expires_at": "2026-11-12T14:31:10.902Z"}}
Verificar a assinatura
Todo envio leva o cabeçalho Int8-Signature: t=<unix>,v1=<hex>. O v1 é o HMAC-SHA256, em hexadecimal, de t, um ponto e o corpo cru, com o segredo inteiro da organização (whsec_…) como chave. O segredo fica em console › Configurações › Webhooks.
import hmac, hashlib, timedef verificar(corpo: bytes, cabecalho: str, segredo: str) -> bool:try:partes = dict(p.split("=", 1) for p in cabecalho.split(","))t, v1 = partes["t"], partes["v1"]if abs(time.time() - int(t)) > 300:return False # mais de 5 min: recusebase = t.encode() + b"." + corpoesperado = hmac.new(segredo.encode(), base, hashlib.sha256).hexdigest()return hmac.compare_digest(esperado.encode(), v1.encode())except (AttributeError, KeyError, ValueError, OverflowError):return False # cabeçalho ausente ou malformado: recuse# Int8-Signature: t=1791937912,v1=5f2c9a…e81d
Confira três coisas: a assinatura bate, o t tem menos de 5 minutos (contra replay) e a comparação é em tempo constante. Use o corpo como chegou, antes de qualquer parse: reformatar o JSON muda os bytes e a assinatura.
Um servidor completo
import jsonimport osfrom flask import Flask, abort, requestfrom verificacao import verificar # a função de verificação acimaapp = Flask(__name__)SEGREDO = os.environ["INT8_WEBHOOK_SECRET"] # whsec_… de console › Configurações › Webhooks@app.post("/int8")def webhook_int8():corpo = request.get_data() # os bytes crus, como foram assinadostry:valido = verificar(corpo, request.headers["Int8-Signature"], SEGREDO)except (KeyError, ValueError):valido = Falseif not valido:abort(400)evento = json.loads(corpo)job = evento["data"]if evento["type"] == "transcription.completed":salvar_transcricao(job["id"], job["result"]["text"])elif evento["type"] == "transcription.failed":avisar_falha(job["id"], job["error"]["code"])return "", 204def salvar_transcricao(job_id: str, texto: str) -> None:...def avisar_falha(job_id: str, codigo: str) -> None:...
Tentativas
Até 3 tentativas: a primeira sai na hora; a segunda, 30 s depois da primeira falhar; a terceira, 5 min depois da segunda falhar. Vale a primeira resposta 2xx. As tentativas pendentes ficam na memória da API: se ela reiniciar no meio, as que faltavam se perdem (o job continua consultável com GET). Cada tentativa é assinada de novo, com o t da hora. Responda rápido (em poucos segundos) e processe depois: uma fila do seu lado evita tentativa repetida por demora.
O mesmo evento pode chegar mais de uma vez (por exemplo, se a sua resposta se perder). Use o data.id do job para não processar duas vezes.
Rotacionar o segredo
Em console › Configurações › Webhooks, "Rotacionar" gera um segredo novo na hora; o anterior para de valer. Atualize o seu servidor logo em seguida.