Terrasonum
VoiceGuard · API v1

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.

Qué no hace la API. No registra voces: cada vocero registra la suya con su propia cuenta, después de aceptar tu invitación. Y no detecta clones de IA. VoiceGuard certifica identidad y fecha; el veredicto describe cuánto se parece un audio a la voz registrada, no si es auténtico.

Empezar

  1. Un administrador de tu organización entra en terrasonum.io → Mi panel → tu organización → Claves de API.
  2. Crea una clave test para integrar. Se muestra una sola vez: guárdala en un gestor de secretos.
  3. 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élivetest
Verificaciones (/verify + /verify-url)1,000 al mes y 10 por hora por organización, sumando todas sus claves live100 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 verificacionessin cuota; no se firma nada
Todas las llamadas300 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áficop50p95
Normal (~70 por hora)4.6 s8.4 s
Pico (~12 por minuto)4.6 s20.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_idRespuesta
00000000-0000-4000-8000-000000000001coincide
00000000-0000-4000-8000-000000000002coincidencia_parcial
00000000-0000-4000-8000-000000000003no_coincide
00000000-0000-4000-8000-000000000004no_concluyente (certificado sin sello de versión)
00000000-0000-4000-8000-000000000005410 certificate_revoked
cualquier otro404 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_idLicencia
00000000-0000-4000-8000-000000000002Vigente: dubbing y video_game, en MX y US, idioma es
00000000-0000-4000-8000-000000000006Vigente: audiobook_narration, en todos los territorios e idiomas, pago por minuto y aprobación de cada uso (202)
00000000-0000-4000-8000-000000000007Revocada: 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.

HTTPcodeCuándo
400invalid_requestFalta un campo o tiene otra forma
400invalid_termsLos términos de una propuesta no son de vcl-1; field dice cuál
403not_authorizedLa licencia no cubre el uso para el que pides credencial; reason dice por qué
409credential_rejectedLa persona rechazó el uso de ese audio
401invalid_key · revoked_keyClave ausente, desconocida o revocada
403organization_suspendedTu organización está suspendida
404not_foundEl certificado no existe o no es de un vocero vigente tuyo
408timeoutNo se pudo descargar el audio de la URL a tiempo
410certificate_revokedEl vocero borró su huella o registró otra; incluye las fechas y el motivo
413 · 415payload_too_large · unsupported_media_typeAudio demasiado grande o en un formato no admitido
422invalid_audioEl audio no se pudo analizar
429quota_exceededTu organización agotó la cuota del mes; resets_at dice cuándo se renueva
429rate_limitedLímite por hora alcanzado (de la organización con clave live, de la clave con clave test); espera lo que diga Retry-After
500 · 502internal_error · upstream_errorFallo nuestro; reintenta más tarde
503capacity_exceededEl servicio está atendiendo el máximo de verificaciones; reintenta tras Retry-After segundos. No gasta cuota
503signing_unavailableNo 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.

  1. 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.
  2. La persona la firma en su panel, con su verificación en dos pasos, o la rechaza. Te llega el webhook voice_license.signed.
  3. 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.
  4. 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}'

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.

EventoCuándo
speaker.joinedUn vocero acepta tu invitación
speaker.certificate_issuedUn vocero registra su voz, o la vuelve a registrar. Dice si el certificado está sellado, con sello de tiempo y firmado
speaker.certificate_revokedEl 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.leftUn vocero se retira (left_by: "speaker") o lo desvinculas ("organization"). Desde ese momento /verify contra él responde 404
voice_license.signedUn 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.revokedUna 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.expiredUna licencia de voz de tu organización llega a su fin sin haberse revocado. Puede tardar unos minutos en llegar
synthetic_use.approvedLa persona aprueba un uso que esperaba su aprobación. data trae la credencial, igual que el 201
synthetic_use.rejectedLa 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

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.