Webhooks
Registrar un endpoint, verificar la firma y por qué para un lote propio conviene más sondear.
webhook_url en POST /v1/transcripts no funciona. webhook_url,
webhook_auth_header y webhook_include_result se rechazan con
400, con un mensaje que apunta aquí.
Hasta hace poco se aceptaban, devolvían 202 y no los leía nadie: el aviso no salía
nunca y el cliente lo daba por configurado. Aceptar algo que no se hace es peor que no
aceptarlo.
Registrar — POST /v1/webhooks
Scope: transcripts:write.
{ "url": "https://mi-app.example.com/callsist", "events": ["transcript.completed", "transcript.failed"] }Solo https. La respuesta trae el secret (whsec_…) una sola vez.
| Evento | Cuándo llega |
|---|---|
transcript.completed | La transcripción está lista para recoger |
transcript.failed | No se pudo procesar. No se factura |
credit.low | El saldo bajó del umbral configurado en el panel. Una vez por cruce |
credit.exhausted | El saldo llegó a cero: desde ese momento todo devuelve 402 |
webhook.disabled | Un endpoint tuyo se apagó tras cinco fallos. No se envía por webhook — el canal que habría que usar es el que acaba de fallar. Va por correo |
api_key.created | Declarado y todavía sin emisor |
Los dos de crédito llegan también por correo a quien tenga el rol de propietario o de facturación, y ahí está el aviso que de verdad evita el susto. El umbral se configura en app.callsist.com/billing y se manda una sola vez al cruzar hacia abajo, así que un lote de cuatrocientas llamadas por debajo del umbral produce un correo, no cuatrocientos.
Otros métodos: GET /v1/webhooks, POST /v1/webhooks/{id}/enable,
DELETE /v1/webhooks/{id}.
El payload no trae la transcripción
Puede pesar megabytes y se reenviaría en cada reintento.
{
"transcript_id": "tr_MoKLFufBqnzJ",
"status": "completed",
"language": "es",
"audio_duration": 216.999
}Al recibirlo, haz GET /v1/transcripts/{transcript_id}.
Verificar la firma
Callsist-Signature: t=1785312782,v1=5f2a9c…Se firma {timestamp}.{cuerpo} con HMAC-SHA256 y el secreto. Tolerancia contra reenvío:
300 segundos.
import hmac, hashlib, time
def verificar(cuerpo: bytes, cabecera: str, secreto: str) -> bool:
partes = dict(p.split("=", 1) for p in cabecera.split(","))
t, recibida = int(partes["t"]), partes["v1"]
if abs(time.time() - t) > 300:
return False
esperada = hmac.new(
secreto.encode(), f"{t}.".encode() + cuerpo, hashlib.sha256
).hexdigest()
return hmac.compare_digest(esperada, recibida)Firma sobre el cuerpo crudo, antes de parsear el JSON. Si tu framework te entrega el objeto ya parseado y lo vuelves a serializar, el HMAC no cuadra nunca: cambia el orden de las claves, los espacios o el escapado.
Reintentos
Cinco: 1 min, 5 min, 30 min, 2 h y 6 h. Tras varios fallos consecutivos el endpoint
se desactiva y se emite webhook.disabled. Responde 2xx rápido y procesa después.
Cuándo no usar webhooks
Para un lote de 400 llamadas propio, sondear es más simple y más robusto: no necesitas endpoint público, ni TLS, ni verificar firmas, y un fallo tuyo no pierde el aviso — al volver, sondeas y ya está. Ver Sondear.
Los webhooks ganan cuando el volumen es continuo y bajo, cuando la latencia importa, o cuando ya tienes un endpoint público que recibe eventos de otros proveedores.