Callsistdocs
Guías

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.

EventoCuándo llega
transcript.completedLa transcripción está lista para recoger
transcript.failedNo se pudo procesar. No se factura
credit.lowEl saldo bajó del umbral configurado en el panel. Una vez por cruce
credit.exhaustedEl saldo llegó a cero: desde ese momento todo devuelve 402
webhook.disabledUn 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.createdDeclarado 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.

On this page