API para organizaciones
Comprueba desde tus propios sistemas si un audio atribuido a uno de tus voceros coincide con la voz que esa persona registró en VoiceGuard. Y, si generas voz con IA, trabaja con las licencias de voz que sus dueños te firman.
Empezar
- Un administrador de tu organización entra en terrasonum.io → Mi panel → tu organización → Claves de API.
- Crea una clave
testpara integrar. Se muestra una sola vez: guárdala en un gestor de secretos. - Cuando todo funcione, crea una clave
live. Solo verifica contra tus voceros vigentes.
curl https://api.terrasonum.io/api/v1/speakers \
-H "Authorization: Bearer tsk_test_…"
Autenticación
Cabecera Authorization: Bearer <clave> en cada llamada. Las claves empiezan por tsk_live_ o tsk_test_. Guardamos solo una huella (SHA-256) de cada clave: si la pierdes, revócala y crea otra. Puede haber hasta 5 activas por modo, para rotarlas sin cortar el servicio.
Las claves son para servidores. No las pongas en una web ni en una app que se ejecute en el dispositivo de otra persona.
Endpoints
Base: https://api.terrasonum.io/api/v1. Contrato completo en openapi.json (OpenAPI 3.1).
GET /speakers
Tus voceros vigentes y el estado de su certificado: si registró su voz, su fecha y si está sellado, con sello de tiempo y firmado. Si borró su huella, la fecha en que se revocó. Es lo mismo que ve tu panel.
POST /verify — archivo
curl https://api.terrasonum.io/api/v1/verify \
-H "Authorization: Bearer tsk_live_…" \
-F certificate_id=<id del certificado del vocero> \
-F file=@audio.m4a
WAV, MP3, FLAC, MP4/M4A, AAC, WebM u OGG, hasta 50 MB. Solo contra certificados de tus voceros vigentes: un certificado de otra persona responde 404, igual que uno que no existe. La respuesta es la misma que la de la verificación pública:
{
"certificate_id": "…",
"verdict": "coincide", // coincide | coincidencia_parcial | no_coincide | no_concluyente
"authenticity_score": 94,
"similarity_score": 0.9412,
"audio_hash": "…", // SHA-256 del audio que enviaste
"same_recording": false, // true = es el mismo archivo del registro (reenvío, no autenticidad)
"sealed": true,
"verdict_rules_version": "veredicto-v3",
"verification_id": "…",
"verified_at": "2026-09-26T…"
}
POST /verify-url — URL pública
curl https://api.terrasonum.io/api/v1/verify-url \
-H "Authorization: Bearer tsk_live_…" \
-H "Content-Type: application/json" \
-d '{"certificate_id":"…","url":"https://…"}'
POST /speaker-invitations
Con {"email": "persona@empresa.com"} crea una invitación de vocero y devuelve el enlace una sola vez. Hazlo llegar a la persona por tu canal. Ella lee qué verá tu organización y qué no, acepta con su propia cuenta (con el correo verificado por Google o Microsoft) y registra su voz. Puede retirarse cuando quiera, sin pedirte permiso; desde ese momento tu clave ya no verifica contra ella.
GET /usage
Uso de la clave por día y operación en los últimos 90 días. Solo cuenta las llamadas que terminaron bien. No guardamos contra qué certificado verificaste. Con una clave live incluye también quota: la cuota de tu organización, lo gastado este mes, lo que queda y cuándo se renueva. consent_quota es lo mismo para consultas y credenciales de licencias de voz.
Límites
| Qué | live | test |
|---|---|---|
Verificaciones (/verify + /verify-url) | 1,000 al mes y 10 por hora por organización, sumando todas sus claves live | 100 por hora y clave, sin cuota |
Licencias de voz (/check + /credentials) | 10,000 al mes y 1,000 por hora por organización, en una cuota aparte de la de verificaciones | sin cuota; no se firma nada |
| Todas las llamadas | 300 cada 15 minutos por clave, y 200 cada 15 minutos por IP | |
Cada verificación live calcula la huella de voz del audio, y la cuota protege ese cálculo. Gastan cuota las verificaciones que llegan a analizar el audio, aunque el audio no sirva (200 y 422). No la gastan las que no llegan (404, 410, 429, 503) ni los fallos nuestros. Los meses y las horas se cuentan en UTC.
Cada verificación live responde con las cabeceras RateLimit-Limit, RateLimit-Remaining y RateLimit-Reset (la hora) y X-Quota-Limit, X-Quota-Used, X-Quota-Remaining y X-Quota-Reset (el mes). El panel de tu organización muestra lo mismo en la tarjeta «Claves de API».
Subimos la cuota por organización cuando hace falta, hasta 50,000 verificaciones al mes: es lo que hemos medido que el servicio aguanta. Pídelo a tu contacto en Terrasonum y dinos cuánto esperas usar al mes y en tu hora más cargada. Con los límites por defecto la API sirve para integrar y para un piloto.
Latencia medida
Prueba de carga del 26 de septiembre de 2026, con audio de 55 segundos: una hora al ritmo de 50,000 al mes (~70 por hora) y diez minutos de pico a ~12 por minuto. 186 verificaciones, ninguna con error.
| Tráfico | p50 | p95 |
|---|---|---|
| Normal (~70 por hora) | 4.6 s | 8.4 s |
| Pico (~12 por minuto) | 4.6 s | 20.1 s |
Es el tiempo de análisis en nuestro servicio. No incluye la subida del audio ni la red entre tus sistemas y los nuestros, y en /verify-url tampoco la descarga del audio. Con audios más cortos tarda menos: el análisis crece con la duración. Si el servicio está atendiendo el máximo, responde 503 capacity_exceeded en el acto, en vez de hacerte esperar.
Entorno de pruebas
Con una clave test nada toca datos reales: no se procesa el audio, no se guardan verificaciones y las invitaciones no se crean. Cada respuesta lleva "test": true. Usa estos certificados de ejemplo:
| certificate_id | Respuesta |
|---|---|
00000000-0000-4000-8000-000000000001 | coincide |
00000000-0000-4000-8000-000000000002 | coincidencia_parcial |
00000000-0000-4000-8000-000000000003 | no_coincide |
00000000-0000-4000-8000-000000000004 | no_concluyente (certificado sin sello de versión) |
00000000-0000-4000-8000-000000000005 | 410 certificate_revoked |
| cualquier otro | 404 not_found |
Licencias de voz de ejemplo, de la organización de prueba. Pasan por las mismas reglas que las reales, pero no firmamos nada: signature llega a null. Una firma nuestra sobre una licencia que no existe afirmaría algo falso. Las propuestas se validan y no se crean.
| license_id | Licencia |
|---|---|
00000000-0000-4000-8000-000000000002 | Vigente: dubbing y video_game, en MX y US, idioma es |
00000000-0000-4000-8000-000000000006 | Vigente: audiobook_narration, en todos los territorios e idiomas, pago por minuto y aprobación de cada uso (202) |
00000000-0000-4000-8000-000000000007 | Revocada: la credencial responde 403 not_authorized con reason: "revoked" |
Errores
Siempre con la forma {"error": {"code": "…", "message": "…"}}. Programa contra code; el texto de message puede cambiar.
| HTTP | code | Cuándo |
|---|---|---|
| 400 | invalid_request | Falta un campo o tiene otra forma |
| 400 | invalid_terms | Los términos de una propuesta no son de vcl-1; field dice cuál |
| 403 | not_authorized | La licencia no cubre el uso para el que pides credencial; reason dice por qué |
| 409 | credential_rejected | La persona rechazó el uso de ese audio |
| 401 | invalid_key · revoked_key | Clave ausente, desconocida o revocada |
| 403 | organization_suspended | Tu organización está suspendida |
| 404 | not_found | El certificado no existe o no es de un vocero vigente tuyo |
| 408 | timeout | No se pudo descargar el audio de la URL a tiempo |
| 410 | certificate_revoked | El vocero borró su huella o registró otra; incluye las fechas y el motivo |
| 413 · 415 | payload_too_large · unsupported_media_type | Audio demasiado grande o en un formato no admitido |
| 422 | invalid_audio | El audio no se pudo analizar |
| 429 | quota_exceeded | Tu organización agotó la cuota del mes; resets_at dice cuándo se renueva |
| 429 | rate_limited | Límite por hora alcanzado (de la organización con clave live, de la clave con clave test); espera lo que diga Retry-After |
| 500 · 502 | internal_error · upstream_error | Fallo nuestro; reintenta más tarde |
| 503 | capacity_exceeded | El servicio está atendiendo el máximo de verificaciones; reintenta tras Retry-After segundos. No gasta cuota |
| 503 | signing_unavailable | No pudimos firmar la consulta o la credencial; reintenta en unos minutos. No gasta cuota |
Licencias de voz
Si generas voz con IA, tu organización puede trabajar con la voz de una persona bajo una licencia que esa persona firmó. Hay cuatro pasos.
- Propones la licencia a quien es dueño de la voz: qué usos, dónde, en qué idiomas, hasta cuándo y en qué condiciones. Lo identificas por el id de su certificado de voz vigente, que esa persona te da.
- La persona la firma en su panel, con su verificación en dos pasos, o la rechaza. Te llega el webhook
voice_license.signed. - Antes de generar, consultas si la licencia cubre lo que vas a hacer. Recibes una respuesta firmada que puedes guardar como prueba de que preguntaste.
- Al generar cada audio, pides una credencial: una prueba firmada de que la licencia cubría ese uso. Va dentro del manifiesto C2PA del audio.
Lo que no aparece en la licencia no está autorizado. Los términos usan el vocabulario vcl-1, y un término desconocido se rechaza: «el que no entiende, no autoriza». Puedes ver el vocabulario en GET https://api.terrasonum.io/api/voice/licenses/vocabulary.
POST /voice-licenses/proposals
curl https://api.terrasonum.io/api/v1/voice-licenses/proposals \
-H "Authorization: Bearer tsk_live_…" -H "Content-Type: application/json" \
-d '{"voice_certificate_id":"…",
"terms":{"uses":["dubbing"],"territories":["MX","US"],"languages":["es"],
"starts_at":"2026-11-01T00:00:00Z","ends_at":"2027-11-01T00:00:00Z",
"compensation_mode":"per_use","compensation_unit":"generated_minute",
"approval":"none","revocation":{"mode":"notice","notice_days":30}},
"private_terms":{"amount":0.8,"currency":"USD","per":"minuto"}}'
private_terms recoge el importe y las condiciones de pago. No se publica: la firma lo cubre solo por su huella. Una propuesta caduca a los 30 días, y puede haber como mucho 5 pendientes con la misma persona.
GET /voice-licenses · GET /voice-licenses/{id}
Devuelven tus licencias con su estado (active, not_started, expired o revoked) y sus términos privados. También incluyen usage: las credenciales emitidas, las que esperan aprobación y los segundos generados. Es el reporte de uso, y no hace falta otro.
POST /voice-licenses/{id}/check
curl https://api.terrasonum.io/api/v1/voice-licenses/<id>/check \
-H "Authorization: Bearer tsk_live_…" -H "Content-Type: application/json" \
-d '{"use":"dubbing","territory":"MX","language":"es-MX"}'
{
"check": {
"type": "voiceguard.consent_check", "license_id": "…",
"use": "dubbing", "territory": "MX", "language": "es-MX",
"result": "authorized", // authorized | not_authorized | approval_required
"reason": null, // unknown_term, use_not_licensed, territory, language,
// not_started, expired, revoked, max_uses_reached | each_use
"authority": "holder", // quién firmó la licencia
"valid_until": "2026-10-06T…", // cachéala hasta aquí (24 h como mucho)
"checked_at": "2026-10-05T…"
},
"signature": "eyJhbGciOiJFUzI1NiJ9…" // JWS ES256 sobre check
}
La respuesta va firmada también cuando no autoriza. Mientras no pase valid_until, puedes reutilizarla sin volver a consultar en cada frase. Así funciona un agente en tiempo real. Si la persona revoca la licencia, el webhook voice_license.revoked llega antes. El idioma es de una licencia cubre es-MX; es-MX no cubre es-ES.
POST /voice-licenses/{id}/credentials
curl https://api.terrasonum.io/api/v1/voice-licenses/<id>/credentials \
-H "Authorization: Bearer tsk_live_…" -H "Content-Type: application/json" \
-d '{"use":"dubbing","territory":"MX","language":"es-MX",
"output_sha256":"<SHA-256 del audio tal como salió del modelo>","duration_sec":42.5}'
201: credencial emitida. Incluyecredential,signature,verify_urlyc2pa_assertion, la aserción lista para tu manifiesto C2PA.200: ya habías pedido la credencial de ese mismo audio. Te devolvemos la misma, y no cuenta como otro uso.202: la licencia exige aprobar cada uso. La persona lo decide en su panel; te llegasynthetic_use.approved, con la credencial, osynthetic_use.rejected. Si no decide en 7 días, la petición caduca.403 not_authorized, conreason: la licencia no cubre ese uso.
duration_sec es obligatorio si la licencia se paga por minuto generado. No enviamos ni guardamos el audio, solo su huella.
Mete c2pa_assertion tal cual en el manifiesto C2PA que firmas al generar el audio:
{
"label": "io.terrasonum.voice-consent",
"data": {
"version": 1,
"credential": "eyJhbGciOiJFUzI1NiJ9…",
"verify_url": "https://terrasonum.io/comprobar-licencia.html#<credential_id>"
}
}
Cualquiera puede comprobar la credencial en verify_url, o pegándola en comprobar-licencia.html. Sin depender de nosotros, se comprueba con node verificar-certificado.js credencial.jws.
Qué prueba una credencial: que, cuando se emitió, había una licencia firmada por quien dice que cubría el uso que tú declaraste. Qué no prueba: que el audio use esa voz ni que el uso real sea el declarado. Lleva el nombre de tu organización: si otro copia tu credencial en el manifiesto de un audio que firma él, el nombre no coincide con quien firmó ese manifiesto. No lleva sello de la TSA: la fecha la afirma nuestra firma.
Webhooks
Si no quieres preguntar cada poco por GET /speakers, Terrasonum puede avisar a tus sistemas cuando algo cambia en tus voceros o en las licencias de voz de tu organización. Los destinos los crea un administrador de la organización en el panel, en la tarjeta «Webhooks»: una URL https:// pública, los eventos que quieres recibir y, como mucho, 3 destinos. La API con clave no gestiona webhooks.
| Evento | Cuándo |
|---|---|
speaker.joined | Un vocero acepta tu invitación |
speaker.certificate_issued | Un vocero registra su voz, o la vuelve a registrar. Dice si el certificado está sellado, con sello de tiempo y firmado |
speaker.certificate_revoked | El certificado de un vocero deja de valer: lo borró (holder) o lo reemplazó por otro (replaced, y llega también el issued del nuevo). Desde ese momento /verify contra él responde 410 |
speaker.left | Un vocero se retira (left_by: "speaker") o lo desvinculas ("organization"). Desde ese momento /verify contra él responde 404 |
voice_license.signed | Un titular firma una licencia de voz que tu organización le propuso. data.license trae la licencia firmada, sin los términos privados |
voice_license.revoked | Una licencia de voz de tu organización se revoca: la revocó el titular (holder) o borró su huella (voice_deleted). revoked_effective_at dice cuándo deja de cubrir |
voice_license.expired | Una licencia de voz de tu organización llega a su fin sin haberse revocado. Puede tardar unos minutos en llegar |
synthetic_use.approved | La persona aprueba un uso que esperaba su aprobación. data trae la credencial, igual que el 201 |
synthetic_use.rejected | La persona rechaza ese uso. data.credential trae lo que declaraste, sin credencial |
No hay evento de verificaciones: tus verificaciones ya te devuelven el resultado en la respuesta, y las que hagan otros contra tus voceros no las verás nunca. Es lo que el vocero aceptó al vincularse.
Cada aviso es un POST con este cuerpo. data.speaker tiene la misma forma que un elemento de GET /speakers:
{
"id": "evt_Qm3v…",
"type": "speaker.certificate_revoked",
"created_at": "2026-10-01T14:03:22.418Z",
"organization_id": "…",
"data": {
"speaker": { "name": "…", "email": "…", "certificate": null, "certificate_revoked_at": "…", … },
"revoked_certificate": { "id": "…", "revoked_at": "…", "reason": "holder" }
}
}
Comprobar la firma
Los avisos siguen la especificación abierta Standard Webhooks, y sus bibliotecas sirven tal cual. Cada uno lleva tres cabeceras: webhook-id (el id del evento), webhook-timestamp (segundos Unix) y webhook-signature. La firma es v1, seguido del HMAC-SHA256 en base64 de <webhook-id>.<webhook-timestamp>.<cuerpo>, con tu secreto whsec_… sin el prefijo y decodificado de base64. Compara con el cuerpo tal como llega, antes de interpretarlo, y rechaza marcas de tiempo con más de 5 minutos de diferencia.
// Node
const crypto = require('crypto');
function verify(secret, headers, rawBody) {
const id = headers['webhook-id'], ts = headers['webhook-timestamp'];
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const key = Buffer.from(secret.slice('whsec_'.length), 'base64');
const expected = crypto.createHmac('sha256', key).update(`${id}.${ts}.${rawBody}`).digest();
return headers['webhook-signature'].split(' ').some((sig) => {
const got = Buffer.from(sig.replace(/^v1,/, ''), 'base64');
return got.length === expected.length && crypto.timingSafeEqual(got, expected);
});
}
# Python
import base64, hashlib, hmac, time
def verify(secret, headers, raw_body: bytes) -> bool:
msg_id, ts = headers["webhook-id"], headers["webhook-timestamp"]
if abs(time.time() - int(ts)) > 300:
return False
key = base64.b64decode(secret.removeprefix("whsec_"))
expected = hmac.new(key, f"{msg_id}.{ts}.".encode() + raw_body, hashlib.sha256).digest()
return any(hmac.compare_digest(base64.b64decode(sig.removeprefix("v1,")), expected)
for sig in headers["webhook-signature"].split(" "))
Al rotar el secreto desde el panel, durante 24 horas cada aviso va firmado con el nuevo y con el anterior (dos firmas separadas por un espacio): cambias el tuyo sin perder ninguno.
Entrega y reintentos
- Responde con cualquier
2xxen menos de 10 segundos. Las redirecciones no se siguen: cuentan como fallo. - Si falla, se reintenta 7 veces más: a los 5 s, 5 min, 30 min, 2 h, 5 h, 10 h y 10 h (unas 27 horas en total).
- Un mismo evento puede llegar más de una vez, y no siempre en orden. Usa
webhook-idpara no procesarlo dos veces yGET /speakerspara el estado actual. - Tras 5 días sin una sola entrega buena, el destino se desactiva. El panel lo muestra y lo reactivas desde ahí.
- El panel enseña las entregas de los últimos 30 días, con el código de cada intento, y permite reenviarlas. Con Enviar prueba recibes un evento de cada tipo con datos de ejemplo y
"test": true.
Qué significa el resultado
El veredicto compara la huella de voz del audio con la que el vocero registró. coincide dice que se parecen mucho; no prueba que el audio sea auténtico, porque una imitación buena también puede parecerse. no_concluyente aparece cuando el certificado es anterior al sello de versión y no se sabe con qué modelo se calculó: se mide, pero no se afirma nada. Cómo se calcula cada veredicto y qué garantiza el certificado está en VoiceGuard para empresas.