Componente JavaScript embebible que muestra el estado de firma de un documento directamente en tu aplicación web, con actualización en tiempo real.
manyao-widget.jsBearer TokenCada 5s (configurable)client_id/client_secret en el frontend. El token del widget lo debe obtener tu backend y pasarlo al frontend.<!-- Contenedor del widget -->
<div id="manyao-widget"></div>
<!-- Script del widget -->
<script src="https://manyao.pe/widget/manyao-widget.js"
data-document="UUID_DEL_DOCUMENTO"
data-token="BEARER_TOKEN"
data-container="manyao-widget">
</script>async function loadManyaoWidget(docUuid) {
// 1. Tu backend obtiene el Bearer token
const resp = await fetch('/tu-api/manyao-token');
const { token } = await resp.json();
// 2. Cargar widget dinámicamente
const script = document.createElement('script');
script.src = 'https://manyao.pe/widget/manyao-widget.js';
script.dataset.document = docUuid;
script.dataset.token = token;
script.dataset.container = 'manyao-widget';
document.body.appendChild(script);
}
loadManyaoWidget('tu-document-uuid');| Atributo | Tipo | Descripción |
|---|---|---|
| data-document | string | UUID del documento a mostrar |
| data-token | string | Bearer token de lectura — no crea documentos ni consume créditos |
| Atributo | Tipo | Descripción | Default |
|---|---|---|---|
| data-container | string | ID del elemento HTML donde renderizar | manyao-widget |
| data-api | string | URL base de la API del widget | https://manyao.pe/v1/widget |
| data-sign-url | string | URL base para enlaces de firma y QR | https://manyao.whatsign.com |
| data-poll | number | Intervalo de polling en ms | 5000 |
| Atributo | Descripción | Default |
|---|---|---|
| data-accent | Color de acento CSS (hex, rgb…). Afecta botones, badges, bordes y panel QR | #0F766E |
| data-title-size | Tamaño de fuente del título del documento | inherit |
| data-title-color | Color del título del documento | inherit |
| data-subtitle-size | Tamaño de fuente del subtítulo / metadatos | inherit |
| data-subtitle-color | Color del subtítulo / metadatos | inherit |
| Atributo | Descripción | Default |
|---|---|---|
| data-qr-open | Panel QR abierto al cargar. false para empezar colapsado | true |
| data-show-add-signer | Mostrar botón "Agregar firmante" | true |
| data-show-remove-signer | Mostrar botón eliminar firmante en cada fila | true |
| data-show-reopen | Mostrar botón "Reabrir" en documentos expirados o parciales | true |
| data-show-partial | Mostrar botón de firma parcial cuando hay firmantes pendientes | true |
| data-show-powered-by | Mostrar el badge "Powered by Manyao" al pie del widget | true |
| data-show-metadata-value | Mostrar valores de metadata del firmante como badges junto al nombre | false |
| Atributo | Descripción | Default |
|---|---|---|
| data-label-partial | Texto del botón de firma parcial | Firma parcial |
| data-label-reopen | Texto del botón reabrir documento | Reabrir |
| data-label-qr-tab | Texto del tab/panel del código QR | Firmar participantes |
// GET /api/widget-token/:docId
app.get('/api/widget-token/:docId', async (req, res) => {
const auth = await fetch('https://api.manyao.pe/v1/auth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: process.env.MANYAO_CLIENT_ID,
client_secret: process.env.MANYAO_CLIENT_SECRET,
grant_type: 'client_credentials'
})
});
const { access_token } = await auth.json();
res.json({ token: access_token, documentId: req.params.docId });
});// GET /api/widget-token.php?doc=UUID
$ch = curl_init('https://api.manyao.pe/v1/auth/token');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode([
'client_id' => getenv('MANYAO_CLIENT_ID'),
'client_secret' => getenv('MANYAO_CLIENT_SECRET'),
'grant_type' => 'client_credentials'
]),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
]);
$resp = json_decode(curl_exec($ch), true);
header('Content-Type: application/json');
echo json_encode([
'token' => $resp['access_token'],
'documentId' => $_GET['doc']
]);En cada ciclo de polling el widget emite un evento manyao:status con el estado completo del documento y los firmantes. Puedes interceptarlo de dos formas según cómo hayas embebido el widget.
document.addEventListener('manyao:status', (event) => {
const { document, signers, session, changed } = event.detail;
console.log('Estado documento:', document.status);
console.log('Firmantes:', signers);
// Solo actuar si hubo cambios reales en este poll
if (changed) {
actualizarMiUI(document, signers);
}
});window.addEventListener('message', (event) => {
// Verificar que el mensaje es de manyao
if (event.data?.type !== 'manyao:status') return;
const { document, signers, session, changed } = event.data;
console.log('Estado documento:', document.status);
console.log('Firmantes:', signers);
if (changed) {
actualizarMiUI(document, signers);
}
});changed: true indica que hubo cambios reales respecto al poll anterior — úsalo para evitar re-renders innecesarios.event.detail o event.data)| Campo | Tipo | Descripción |
|---|---|---|
| type | string | Siempre "manyao:status" |
| documentId | string | UUID del documento |
| document | object | Estado completo del documento — ver tabla abajo |
| signers | array | Lista de firmantes — ver tabla abajo |
| session | object | Estado del token de sesión — ver tabla abajo |
| changed | boolean | true si hubo cambios desde el poll anterior (estado, firmantes o PDF) |
document| Campo | Tipo | Descripción |
|---|---|---|
| uuid | string | UUID del documento |
| title | string | Título del documento |
| company | string | Nombre de la empresa emisora |
| status | string | Estado del documento: pending · signing · completed · partial · cancelled · expired |
| total | number | Total de firmantes del documento |
| completed | number | Cantidad de firmantes que completaron la firma |
| can_generate_pdf | boolean | true si todos los firmantes completaron y se puede generar el PDF final |
| has_signed_pdf | boolean | true si el PDF firmado ya fue generado y está disponible |
| pdf_generating | boolean | true si el PDF está siendo generado en este momento |
| has_pdf | boolean | true si existe un PDF original adjunto al documento |
| pdf_size_formatted | string|null | Tamaño del PDF original formateado (ej: "369 KB") |
| page_count | number|null | Número de páginas del PDF |
| created_at | string | Fecha de creación ISO 8601 |
| expires_at | string|null | Fecha de expiración ISO 8601, o null si no expira |
| signing_mode | string | closed (lista cerrada) · open (cualquiera puede firmar) |
| signing_blocked | boolean | true si las nuevas firmas están bloqueadas temporalmente |
| allow_inspector_assist | boolean | true si se permite la firma asistida por inspector |
signers[]| Campo | Tipo | Descripción |
|---|---|---|
| id | number | ID interno del firmante |
| dni | string | Número de documento del firmante |
| doc_type | string | Tipo de documento: dni-pe · pe-carnet · ci-pe · dni-pa |
| name | string|null | Nombre completo verificado biométricamente, o null si aún no firmó |
| title | string|null | Cargo o rol del firmante (opcional, definido al crear) |
| metadata | object|null | Datos extra del firmante definidos al crear el documento |
| status | string | pending · identifying · signing · waiting_review · completed · rejected · declined |
| signed_at | string|null | Timestamp ISO 8601 de cuando firmó, o null si aún no completó |
| signer_step | string|null | Paso actual dentro del flujo de firma (uso interno) |
| step_since | string|null | Timestamp de cuando entró al paso actual |
session| Campo | Tipo | Descripción |
|---|---|---|
| token_expired | boolean | true si el Bearer token expiró — el widget pasa a modo solo lectura |
| token_expires_at | string|null | Fecha de expiración del token ISO 8601 |
| read_only | boolean | true si la sesión está en modo solo lectura (token expirado o documento en estado terminal) |
| actions_allowed | string[] | Acciones disponibles: view_document · download_pdf · add_signer · remove_signer · share_qr · share_link |
| actions_blocked | string[] | Acciones bloqueadas en esta sesión |
document.addEventListener('manyao:status', (event) => {
const { document, signers, changed } = event.detail;
// Actualizar contador en tu UI
document.getElementById('progreso').textContent =
`${document.completed} de ${document.total} firmantes`;
// Detectar cuando se completó el documento
if (document.status === 'completed' && document.has_signed_pdf) {
document.getElementById('descargar').style.display = 'block';
mostrarNotificacion('¡Documento firmado por todos!');
}
// Detectar firmante en revisión manual
const enRevision = signers.find(s => s.status === 'waiting_review');
if (enRevision) {
mostrarAlerta(`${enRevision.name || enRevision.dni} requiere revisión manual`);
}
});El widget emite el evento manyao:action cada vez que el emisor realiza una acción (aprobar, agregar firmante, descargar, etc.). A diferencia de manyao:status que se emite en cada poll, este evento es puntual — se dispara solo cuando ocurre la acción.
document.addEventListener('manyao:action', (event) => {
const { action, documentId, document, signers, data } = event.detail;
switch (action) {
case 'signer_added':
console.log('Firmante agregado:', data.dni);
break;
case 'signer_removed':
console.log('Firmante eliminado:', data.signer_id);
break;
case 'signer_review_approve':
console.log('Firmante aprobado:', data.signer_id);
break;
case 'document_download':
console.log('Documento descargado');
break;
}
});window.addEventListener('message', (event) => {
if (event.data?.type !== 'manyao:action') return;
const { action, documentId, document, signers, data } = event.data;
console.log('Acción:', action, data);
});document y signers con el estado actual en el momento de la acción — no necesitas hacer un poll adicional.manyao:action| Campo | Tipo | Descripción |
|---|---|---|
| type | string | Siempre "manyao:action" |
| action | string | Identificador de la acción — ver tabla abajo |
| documentId | string | UUID del documento |
| document | object | Estado del documento en el momento de la acción |
| signers | array | Lista de firmantes en el momento de la acción |
| data | object | Datos adicionales específicos de la acción — ver tabla abajo |
| action | Cuándo se emite | Campos en data |
|---|---|---|
| signer_review_approve | Firmante aprobado desde revisión manual | signer_id, action: "approve" |
| signer_review_reject | Firmante rechazado desde revisión manual | signer_id, action: "reject" |
| signer_review_retry | Firmante reiniciado desde revisión manual | signer_id, action: "retry" |
| signer_reset | Firmante reiniciado manualmente | signer_id |
| signer_added | Nuevo firmante agregado al documento | doc_type, dni |
| signer_removed | Firmante eliminado del documento | signer_id |
| document_view | Se abrió el visor del PDF | {} |
| document_download | Se descargó el PDF del documento | {} |
| signer_pdf_download | Se descargó el certificado de un firmante | signer_id |
| cert_view | Se abrió el visor del certificado | {} |
| document_reopen | Documento reactivado/reabierto | extend_hours |
// Escuchar cambios de estado (polling)
document.addEventListener('manyao:status', (e) => {
const { document, signers, changed } = e.detail;
if (changed) actualizarUI(document, signers);
});
// Escuchar acciones puntuales del emisor
document.addEventListener('manyao:action', (e) => {
const { action, data, document, signers } = e.detail;
if (action === 'signer_added') {
mostrarToast(`Firmante ${data.dni} agregado`);
actualizarUI(document, signers);
}
if (action === 'signer_review_approve') {
mostrarToast('Firmante aprobado ✅');
actualizarUI(document, signers);
}
if (action === 'document_download') {
registrarDescarga(document.uuid);
}
});