API pública de verificación
PulseID expone 4 endpoints REST de solo lectura para verificar, desde cualquier sistema externo, la evidencia fisiológica que emite el hub beta: sellos individuales, eventos certificados completos, anclaje en Bitcoin (OpenTimestamps) y pruebas de conocimiento cero (ZK-BP). Todos son públicos, no requieren API key y devuelven JSON — los identificadores que reciben (hash de sello, proof_ref, certificate ID) son anónimos y no contienen datos personales ni fisiológicos crudos.
GET/api/verify?s=<sealHash>
Verifica un sello individual (SHA-256 de una operación sellada en el hub beta). Es el endpoint que escanea la cámara de un teléfono al leer un QR de certificado, y el que usa el panel “Bridge API” embebido en /beta. Si la petición trae Accept: text/html (un navegador, no una integración programática) devuelve una tarjeta HTML en vez de JSON — pensado para cuando alguien escanea el QR directamente con la cámara.
Parámetros| Nombre | Tipo | Requerido | Descripción |
|---|
| s | string (64 hex) | sí | Hash SHA-256 del sello a verificar. |
curlcurl "https://pulseids.com/api/verify?s=<sealHash>"
Response 200 (sello válido){
"valid": true,
"source": "database",
"certificateId": "a1b2c3d4e5f6a7b8",
"issuedAt": "2026-07-10T14:32:00.000Z",
"program": "personal"
}
Response 404 (no encontrado){
"valid": false,
"error": "Sello no encontrado"
}
Códigos de respuesta| HTTP | error / result | Significado |
|---|
| 400 | Parámetro s requerido | El parámetro s falta o no son 64 caracteres hexadecimales. |
| 404 | Sello no encontrado | No existe ningún sello con ese hash. |
| 502 | Error consultando la base de datos | Fallo transitorio de Supabase. |
| 503 | Servicio de verificación no configurado | Faltan variables de entorno en el despliegue. |
| 500 | Error interno de verificación | Excepción no controlada. |
CORS abierto (Access-Control-Allow-Origin: *) · sin autenticación · sin rate limit propio en esta rama GET.
POST/api/verify
Verifica un token PID (Pulse Identity Document) firmado con HMAC-SHA256 por un dispositivo registrado — formato header.payload.sig. A diferencia del GET anterior (que solo confirma que un sello existe en base de datos), este recalcula la firma HMAC contra la clave del dispositivo y comprueba caducidad, estado fisiológico (COHERENT) y longitud de la cadena de prueba. Es el endpoint pensado para integraciones B2B que ya poseen un token PID (por ejemplo, recibido de un usuario) y quieren verificarlo por su cuenta.
Parámetros| Nombre | Tipo | Requerido | Descripción |
|---|
| token | string | sí | Token PID completo: header.payload.sig (base64url). |
| did | string (16 hex) | sí | Identificador del dispositivo que firmó el token. |
curlcurl -X POST https://pulseids.com/api/verify \
-H "Content-Type: application/json" \
-d '{
"token": "<header>.<payload>.<sig>",
"did": "a1b2c3d4e5f6a7b8"
}'
Response 200 (result: VALID){
"result": "VALID",
"valid": true,
"device_label": "Amazfit Balance",
"payload": {
"sub": "...",
"iat": 1752345600000,
"exp": 1752347400000,
"state": "COHERENT",
"uv": 1,
"proof": { "chain": 4 },
"pid": "..."
},
"verified_at": "2026-07-12T10:00:00.000Z",
"message": "Identidad fisiológica verificada. Cadena criptográfica: 4 eslabones."
}
Response 200 (result: INVALID_STATE, ejemplo de fallo de dominio){
"result": "INVALID_STATE",
"valid": false,
"payload": { "...": "..." },
"verified_at": "2026-07-12T10:00:00.000Z",
"message": "Estado fisiológico insuficiente: BASELINE. Se requiere COHERENT."
}
Valores posibles de “result”| result | valid | Causa |
|---|
| VALID | true | Firma, caducidad, estado y cadena de prueba correctos. |
| MALFORMED | false | Body inválido, token sin 3 partes, o header con typ/alg inesperado. |
| EXPIRED | false | El token superó su exp (TTL de 30 minutos). |
| UNKNOWN_DEVICE | false | El did no está registrado en PulseID. |
| INVALID_SIG | false | La firma HMAC no coincide con la clave del dispositivo. |
| INVALID_STATE | false | Estado fisiológico distinto de COHERENT, o cadena de prueba vacía. |
| RATE_LIMITED | false | Más de 60 verificaciones/min desde la misma IP (ver abajo). |
Códigos de respuesta| HTTP | error / result | Significado |
|---|
| 400 | MALFORMED | Body JSON inválido, o faltan los campos token/did. |
| 405 | Method not allowed | Método distinto de POST/OPTIONS. |
| 429 | RATE_LIMITED | Límite de 60 peticiones/min por IP superado — header Retry-After indica cuándo reintentar. |
Nota de diseño: los fallos de verificación de dominio (token caducado, firma inválida, estado insuficiente…) devuelven HTTP 200 con valid: false — no son errores de transporte, léase siempre el campo result/valid, no solo el status code.
Rate limit: 60 peticiones/min por IP (clave verify en el limitador). CORS abierto (Access-Control-Allow-Origin: *) — antes de 2026-07-12 este POST estaba restringido a PUBLIC_BASE_URL, lo que bloqueaba llamadas desde el navegador de un integrador externo aunque la operación fuera de solo lectura y sin cookies; se alineó con el resto de la familia.
GET/api/verify-proof?proof_ref=<ref>
Verifica un evento certificado completo: recalcula el hash del payload y la firma HMAC, comprueba caducidad, y devuelve una vista pública (publicView) con banderas explícitas de qué se expone (safe_mode, physiological_raw_exposed…). Es el endpoint más completo de los cuatro — a diferencia de GET /api/verify, no solo confirma existencia en base de datos, sino que revalida la evidencia criptográfica en cada petición.
Parámetros| Nombre | Tipo | Requerido | Descripción |
|---|
| proof_ref | string | sí | Referencia de prueba del evento certificado. |
curlcurl "https://pulseids.com/api/verify-proof?proof_ref=<proof_ref>"
Response 200 (verification_status: valid){
"ok": true,
"proof_ref": "abc123",
"session_id": "sess_...",
"issuer": "amazfit_balance",
"channel": "amazfit_balance_watchface",
"verification_status": "valid",
"state": "COHERENT",
"hr_band": "60-70",
"issued_at": "2026-07-10T14:32:00.000Z",
"verified_at": "2026-07-12T10:00:00.000Z",
"expires_at": "2026-08-10T14:32:00.000Z",
"device_ref_public": "a1b2c3d4...ab12",
"payload_hash_public": "9f8e7d6c...34ff",
"signature_public": "3c2b1a09...ff01",
"signature_alg": "HMAC-SHA256",
"pdf_available": true,
"pdf_url": "https://.../certificado.pdf",
"verify_url": "https://pulseids.com/v/abc123",
"safe_mode": true,
"physiological_raw_exposed": false,
"internal_metrics_exposed": false,
"token_full_exposed": false,
"error": null
}
Response 404 (no encontrado){
"ok": false,
"verification_status": "not_found",
"error": "not_found",
"pdf_available": false
}
Códigos de respuesta| HTTP | error / result | Significado |
|---|
| 400 | missing_proof_ref | Falta el parámetro proof_ref. |
| 404 | not_found | No existe ningún evento certificado con ese proof_ref. |
| 500 | verification_lookup_failed / verification_failed | Fallo de Supabase o excepción no controlada. |
verification_status también puede valer tampered (el hash del payload no coincide), invalid_signature o expired — en esos casos ok es false pero el HTTP sigue siendo 200, igual que en el POST de /api/verify. CORS abierto · sin autenticación · sin rate limit propio.
GET/api/ots-verify?seal=<certificateId>
Estado del anclaje Bitcoin (OpenTimestamps) de un certificado: si el hash de la sesión llegó a incluirse en un bloque de Bitcoin vía los calendar servers de OpenTimestamps, y desde cuándo. No revalida criptografía en cada llamada — lee el recibo (ots_receipt) ya persistido por seal-anchor.js / el reintento horario ots-anchor-retry.mjs.
Parámetros| Nombre | Tipo | Requerido | Descripción |
|---|
| seal | string (16 hex) | sí | certificate_id del sello (pulseid_beta_certificates). |
curlcurl "https://pulseids.com/api/ots-verify?seal=<certificateId>"
Response 200 (anclado){
"ok": true,
"sealId": "a1b2c3d4e5f6a7b8",
"issuedAt": "2026-07-10T14:32:00.000Z",
"anchored": true,
"otsAnchoredAt": "2026-07-10T15:10:00.000Z",
"downloadUrl": "/api/ots-download?seal=a1b2c3d4e5f6a7b8"
}
Response 200 (pendiente de anclar){
"ok": true,
"sealId": "a1b2c3d4e5f6a7b8",
"issuedAt": "2026-07-10T14:32:00.000Z",
"anchored": false,
"otsAnchoredAt": null,
"downloadUrl": null
}
Códigos de respuesta| HTTP | error / result | Significado |
|---|
| 400 | invalid_seal_id | seal no son 16 caracteres hexadecimales. |
| 404 | seal_not_found | No existe ningún certificado con ese certificate_id. |
| 503 | service_unavailable | Supabase no configurado en el despliegue. |
| 500 | db_error | Fallo consultando Supabase. |
Si
anchored: true,
downloadUrl apunta a
GET /api/ots-download?seal=…, que devuelve el fichero
.ots crudo verificable de forma independiente en
opentimestamps.org o con el cliente oficial
ots — sin necesidad de confiar en PulseID. Página de verificación visual equivalente:
/ots?seal=…. CORS abierto · sin autenticación · sin rate limit propio.
GET/api/zkbp-verify?cert=<certificateId>
Prueba de conocimiento cero (ZK-BP, Pedersen + Sigma sobre secp256k1) de que la coherencia fisiológica de una sesión superó el umbral exigido por su programa de cumplimiento, sin revelar el valor real. A diferencia de /api/ots-verify, aquí la verificación criptográfica completa se re-ejecuta en cada petición (barata, sin llamadas externas) en vez de fiarse de un flag persistido.
Parámetros| Nombre | Tipo | Requerido | Descripción |
|---|
| cert | string (16 hex) | sí | certificate_id del sello (pulseid_beta_certificates). |
curlcurl "https://pulseids.com/api/zkbp-verify?cert=<certificateId>"
Response 200 (prueba disponible y verificada){
"ok": true,
"certificateId": "a1b2c3d4e5f6a7b8",
"issuedAt": "2026-07-10T14:32:00.000Z",
"program": "personal",
"threshold": 0.75,
"proto": "zkbp-secp256k1-v1",
"available": true,
"verified": true,
"commitment": "02ab3c...",
"chainHash": "9f8e7d6c...",
"proof": {
"proto": "zkbp-secp256k1-v1",
"commitment": "02ab3c...",
"bitsD": [ "/* rango-proof, ver _shared/zkbp.js */" ],
"bitsV": [ "/* rango-proof, ver _shared/zkbp.js */" ]
}
}
Response 200 (certificado sin ZK-BP asociado){
"ok": true,
"certificateId": "a1b2c3d4e5f6a7b8",
"issuedAt": "2026-07-10T14:32:00.000Z",
"program": "personal",
"available": false,
"verified": false
}
Códigos de respuesta| HTTP | error / result | Significado |
|---|
| 400 | invalid_certificate_id | cert no son 16 caracteres hexadecimales. |
| 404 | certificate_not_found | No existe ningún certificado con ese certificate_id. |
| 503 | service_unavailable | Supabase no configurado en el despliegue. |
| 500 | db_error | Fallo consultando Supabase. |
commitment, chainHash y proof se sirven completos a propósito: no revelan el valor real (ese es el punto de una prueba de conocimiento cero), y permiten que un tercero reverifique por su cuenta sin confiar en este endpoint. Página de verificación visual equivalente: /zk?cert=…. CORS abierto · sin autenticación · sin rate limit propio.
GET/.well-known/pulseid-verify?ref=<identificador>
Punto de entrada único pensado para un crawler o un agente automatizado (incluidos LLMs con herramientas web) que aterriza en pulseids.com sin contexto previo y no puede — o no quiere — interpretar el HTML de esta misma página. Sigue el patrón .well-known/ (como security.txt o ai-plugin.json en otros sitios): llamado sin parámetros devuelve un manifiesto JSON plano y autodescriptivo de qué es PulseID y qué identificadores acepta; llamado con ?ref= detecta automáticamente el formato del identificador (no hace falta saber de antemano si es un certificate_id, un hash de sello o un proof_ref) y hace de proxy hacia el endpoint real correspondiente — reutilizando exactamente la misma lógica de verificación que /api/verify, /api/verify-proof y /api/zkbp-verify, sin duplicarla.
Parámetros| Nombre | Tipo | Requerido | Descripción |
|---|
| ref | string | no | Identificador a verificar. Omitido → devuelve el manifiesto. certificate_id (16 hex), seal_hash (64 hex) o proof_ref (alfanumérico, sin puntos) → devuelve el resultado de verificación. |
curl — manifiesto (sin parámetros)curl "https://pulseids.com/.well-known/pulseid-verify"
Response 200 (manifiesto, forma abreviada){
"name": "PulseID",
"description": "PulseID emite identidad fisiológica certificable...",
"identifier_types": [
{ "type": "certificate_id", "format": "16 hex", "real_endpoint": { "url": ".../api/zkbp-verify?cert=..." } },
{ "type": "seal_hash", "format": "64 hex", "real_endpoint": { "url": ".../api/verify?s=..." } },
{ "type": "proof_ref", "format": "alfanumérico, sin puntos", "real_endpoint": { "url": ".../api/verify-proof?proof_ref=..." } }
]
}
curl — auto-detección (cualquiera de los 3 formatos)curl "https://pulseids.com/.well-known/pulseid-verify?ref=<identificador>"
Response 200 (ref = certificate_id, 16 hex){
"identifier_type": "certificate_id",
"ok": true,
"verified": true,
"certificate_id": "a1b2c3d4e5f6a7b8",
"issued_at": "2026-07-10T14:32:00.000Z",
"program": "personal",
"human_readable_summary": "Verificado: el certificado a1b2c3d4e5f6a7b8 (programa personal) prueba mediante conocimiento cero que la coherencia fisiológica de la sesión superó el umbral requerido, sin revelar el valor real.",
"source_endpoint": "https://pulseids.com/api/zkbp-verify?cert=a1b2c3d4e5f6a7b8"
}
Response 400 (formato no reconocido){
"ok": false,
"error": "unrecognized_identifier_format",
"human_readable_summary": "No se reconoce el formato del identificador enviado en `ref`. Se aceptan 3 formatos...",
"accepted_formats": [ "..." ]
}
El campo human_readable_summary está en español y pensado para que un LLM lo cite directamente sin tener que interpretar el resto de campos estructurados. Nota: certificate_id (ZK-BP) comparte formato (16 hex) con el seal de /api/ots-verify — el estado de anclaje en Bitcoin no está cubierto por la auto-detección de ?ref=, llama a /api/ots-verify directamente si buscas específicamente eso. CORS abierto · sin autenticación · sin rate limit propio (mismo criterio que sus hermanos de solo lectura).
SCRIPT/pulseid-badge.js
Script embebible sin dependencias que pinta un badge "✓ Verificado por PulseID" en cualquier web de terceros, consultando GET /api/verify-proof en vivo (sin iframe, sin backend propio del anfitrión). Pensado para que empresas certificadas muestren su estado de verificación fuera de PulseID — widget de confianza de la criba de impacto, distribución sin coste de adquisición.
Embeber (un badge)<span data-pulseid-badge="<proof_ref>"></span>
<script src="https://pulseids.com/pulseid-badge.js" defer></script>
Varios badges en la misma página<span data-pulseid-badge="<proof_ref_1>"></span>
<span data-pulseid-badge="<proof_ref_2>"></span>
<script src="https://pulseids.com/pulseid-badge.js" defer></script>
Parámetros| Nombre | Tipo | Requerido | Descripción |
|---|
| data-pulseid-badge | string | sí | proof_ref del evento certificado a verificar (mismo identificador que consume /api/verify-proof). |
| data-pulseid-theme | "dark" | "light" | no | Tema del chip. Por defecto "dark" — no depende del CSS del sitio anfitrión. |
| data-pulseid-size | "sm" | "md" | no | Tamaño del chip. Por defecto "md". |
Tres estados visuales: cargando (punto pulsante), verificado (✓ verde, enlaza a verify_url) y no verificado (✕ rojo, con el motivo — caducado, firma inválida, no encontrado...). Construido con createElement/textContent, nuncainnerHTMLcon datos de red. Degrada a un chip "No se pudo verificar" en vez de lanzar excepciones si la petición falla. API programática: window.PulseIDBadge.mount(el, proofRef) para montar tras el load inicial (SPAs). Fuente y documentación completa (comentario de cabecera) en public/pulseid-badge.js.
SCRIPT/pulseid-badge-wc.js
Misma verificación en vivo contra GET /api/verify-proof, empaquetada como Custom Element (<pulseid-badge>) con Shadow DOM real en vez de escaneo de [data-pulseid-badge] sobre el DOM global. Pensado para integradores que construyen su propia web a base de componentes: el Shadow DOM aísla el CSS del badge del sitio anfitrión (y viceversa) y el elemento se monta/desmonta con el ciclo de vida estándar de Custom Elements, sin necesidad de re-escanear la página.
Embeber (Web Component)<script src="https://pulseids.com/pulseid-badge-wc.js" defer></script>
<pulseid-badge proof-ref="<proof_ref>" theme="dark" size="md"></pulseid-badge>
Varios badges en la misma página<pulseid-badge proof-ref="<proof_ref_1>"></pulseid-badge>
<pulseid-badge proof-ref="<proof_ref_2>"></pulseid-badge>
<script src="https://pulseids.com/pulseid-badge-wc.js" defer></script>
Parámetros| Nombre | Tipo | Requerido | Descripción |
|---|
| proof-ref | string | sí | proof_ref del evento certificado a verificar (mismo identificador que consume /api/verify-proof). |
| theme | "dark" | "light" | no | Tema del chip. Por defecto "dark" — no depende del CSS del sitio anfitrión. |
| size | "sm" | "md" | no | Tamaño del chip. Por defecto "md". |
Mismos tres estados visuales y mismo criterio de seguridad que pulseid-badge.js (nunca innerHTML con datos de red, siempre createElement/textContent, ahora escritos dentro del Shadow Root del elemento). Cada instancia de <pulseid-badge> tiene su propio Shadow DOM aislado, así que varios badges en la misma página no comparten estilos ni estado entre sí. Cambiar proof-ref en caliente (el.setAttribute('proof-ref', otroRef)) re-dispara la verificación automáticamente vía attributeChangedCallback; cambiar solo theme o size repinta con el último resultado ya obtenido sin volver a golpear la red. No sustituye a pulseid-badge.js — ambos scripts coexisten como dos formas de integración para casos de uso distintos (HTML plano vs. arquitectura basada en componentes). Fuente y documentación completa (comentario de cabecera) en public/pulseid-badge-wc.js.
Convenciones generales
Formato de respuestaLos cuatro endpoints no comparten un envelope único — es un rasgo de que evolucionaron por separado, no un contrato de API versionado. GET /api/verify usa { valid, ... }; los otros tres usan { ok, ... }. Si integras contra varios, no asumas el mismo campo de éxito en todos — compruébalo endpoint por endpoint contra las tablas de esta página.
Autenticación y cuotasNinguno de los cuatro requiere API key hoy — son de lectura pública sobre identificadores anónimos. El único con rate limit propio es POST /api/verify (60/min por IP). Si vas a integrar tráfico de volumen (verificación en cada request de tu backend, por ejemplo), cachea por proof_ref/certificate_id en tu lado — estos identificadores son inmutables una vez emitidos.
EstabilidadEstos son los contratos ya en producción, documentados tal cual — no hay todavía un endpoint unificado versionado (tipo /api/v1/verify) que envuelva a los cuatro. Si tu integración necesita ese nivel de estabilidad a largo plazo, contacta antes de depender de la forma exacta de la respuesta.