Guía de integración (end-to-end)
Recorrido completo para integrar el CMP de DPOLab en un sitio web real. Está pensado para un desarrollador técnico que recibe el encargo de su DPO y necesita pasar de cero a primer consentimiento grabado en menos de 30 minutos, sin asistencia.
Asume que el DPO ya creó al menos un aviso en el dashboard y te pasó el widgetKey. Si no, lee primero la guía del módulo CMP.
Decisión 1: ¿qué tipo de aviso necesitas?
El CMP cubre cuatro casos típicos. Cada uno tiene su propio snippet y su propio canal en el dashboard. Empieza por identificar cuál te toca:
| Caso | Canal | Componente del SDK | Cuándo usarlo |
|---|---|---|---|
| Banner de cookies en tu sitio | WEB_SDK | DPOLab.init() | Sitio público con cookies de analytics, marketing, retargeting. El estándar de la industria. |
| Checkbox "Acepto la política" en un formulario | WEB_SDK | DPOLab.renderConsentCheckbox() | Formularios de contacto, descarga de recursos, registro, newsletter. |
| URL pública compartible (link/QR) | FORM_LINK | URL del dashboard | Consentimiento por email, WhatsApp, QR físico, cartel de videovigilancia. Sin código. |
| Sistema externo (CRM, e-commerce) envía consentimientos por webhook | WEBHOOK | curl + HMAC | Integraciones server-to-server donde el dato viene de fuera (ej. Shopify Checkout, HubSpot). |
Más de uno puede coexistir en el mismo aviso si tu caso lo amerita. Cuando dudes, empieza por uno y agrega los otros después — son aditivos.
Caso A — Banner de cookies en tu sitio
Paso 1: registra tu dominio
Antes de poder usar el snippet de producción, el operador (tu DPO) debe agregar tu dominio a Dashboard → Settings → Dominios y vincularlo al aviso. Sin esto, el SDK responde con 403 (origin denied).
Si todavía estás iterando y no quieres pelear con la lista de dominios, usa el snippet sandbox que tiene su propia sección abajo.
Paso 2: copia el snippet de producción
Desde Dashboard → CMP → tu aviso → tab Integración, copia el snippet rotulado PRODUCCIÓN (badge verde) y pégalo en el <head> de tu sitio:
<!-- DPOLab CMP — Banner de cookies + script blocking -->
<script src="https://api.dpolab.com/sdk/cmp-sdk.js"></script>
<script>
DPOLab.init({
widgetKey: 'tu-widget-key-aqui',
apiUrl: 'https://api.dpolab.com',
appUrl: 'https://app.dpolab.com',
blocking: true, // Bloquea scripts hasta obtener consentimiento
gtmConsent: true, // Google Tag Manager Consent Mode v2
});
</script>Paso 3: marca tus scripts de tracking
Para que el SDK bloquee scripts hasta tener consentimiento, cambia el type="text/javascript" a type="text/plain" y agrega el atributo data-dpolab-consent con el slug exacto del propósito declarado en el aviso:
<!-- Google Analytics: solo se carga si el visitante acepta "analytics" -->
<script type="text/plain" data-dpolab-consent="analytics"
src="https://www.googletagmanager.com/gtag/js?id=G-XXXXX"></script>
<!-- Facebook Pixel: solo si acepta "marketing" -->
<script type="text/plain" data-dpolab-consent="marketing"
src="https://connect.facebook.net/en_US/fbevents.js"></script>Los slugs analytics y marketing del ejemplo deben existir literalmente en los propósitos del aviso. Si tu aviso tiene analytics_ga4 en vez de analytics, el script de GA queda bloqueado para siempre aunque el visitante acepte "todo". Los slugs los ves en el detalle del aviso, sección Propósitos.
Paso 4: prueba antes de salir a producción
Hay tres formas, en orden de menor a mayor esfuerzo:
-
Preview interna del dashboard (
Dashboard → CMP → tu aviso → tab Integración → Sandbox → Abrir preview). Muestra el banner sobre un mock de sitio. Sirve para ver colores, copy y posición pero no su comportamiento real sobre tu CSS. -
Snippet sandbox en una página HTML local. Más realista. El snippet sandbox tiene un único cambio vs el de producción:
sandbox: true. Salta el origin check y no persiste la decisión del visitante, así que puedes recargar y ver el banner cada vez sin limpiar nada. -
Túnel HTTPS hacia tu localhost (ngrok, cloudflared). Para probar contra un sitio público (ej. tu staging en Vercel) consumiendo el API local.
cloudflared tunnel --url http://localhost:3001te da una URL HTTPS pública.
El snippet sandbox no es para producción. En sandbox el banner reaparece en cada refresh (no recuerda la decisión del visitante) y los consentimientos no entran en tus métricas. Cuando termines de iterar, reemplaza sandbox: true por el snippet de producción.
Paso 5: valida que el blocking funciona
En tu sitio con el snippet PRODUCCIÓN:
- Abre DevTools → Network.
- Carga la página por primera vez (sin consentimiento previo).
- Confirma que los scripts marcados con
data-dpolab-consentNO aparecen en Network. - Haz clic "Aceptar todo" en el banner.
- Actualiza la pestaña Network: ahora SÍ deben aparecer los scripts de tracking.
Si el blocking no funciona, verifica:
blocking: trueestá enDPOLab.init().- El slug del
data-dpolab-consentcoincide letra a letra con el slug del propósito (ver Paso 3). - El script está en el
<head>antes del primer render, no inyectado dinámicamente después.
Caso B — Checkbox de consentimiento en un formulario
Paso 1: copia el snippet de checkbox
Desde el dashboard, en tab Integración → bloque Checkbox, copia el snippet:
<!-- DPOLab — Checkbox de consentimiento -->
<div id="dpolab-consent"></div>
<script src="https://api.dpolab.com/sdk/cmp-sdk.js"></script>
<script>
DPOLab.renderConsentCheckbox({
widgetKey: 'tu-widget-key-aqui',
purpose: 'privacy_policy',
label: 'He leído y acepto la',
linkText: 'Política de Privacidad',
linkUrl: '/privacidad',
required: true,
submitButtonId: 'submit-btn',
emailFieldId: 'email',
}, 'dpolab-consent');
</script>Paso 2: prepara tu formulario
El SDK necesita 3 IDs en el DOM:
- Un
<div>donde renderizar el checkbox (dpolab-consenten el snippet). - El
<input type="email">para capturar el email del titular (IDV nivel 1+). - El
<button type="submit">para deshabilitar hasta que se acepte (sirequired: true).
<form id="mi-formulario">
<input type="text" id="nombre" name="nombre" required>
<input type="email" id="email" name="email" required>
<!-- El checkbox de DPOLab se renderiza acá -->
<div id="dpolab-consent"></div>
<button type="submit" id="submit-btn">Enviar</button>
</form>Paso 3: engancha el submit (importante)
Desde mayo 2026, el SDK no graba el consentimiento al marcar el checkbox. Lo hace cuando tú llamas DPOLab.recordConsent() en el submit. Esto evita consentimientos huérfanos (usuario que marca pero nunca envía).
document.getElementById('mi-formulario').addEventListener('submit', async function(e) {
e.preventDefault();
// 1. Validar el checkbox (muestra borde rojo si no está marcado)
if (!DPOLab.validateConsent('privacy_policy')) return;
// 2. Grabar el consentimiento (lee el email del campo)
const recorded = await DPOLab.recordConsent('privacy_policy');
if (!recorded) return; // falla si falta email (L1) o el backend rechaza
// 3. Enviar tu form normalmente
fetch('/api/contact', { method: 'POST', body: new FormData(e.target) });
});El detalle completo de recordConsent y el comportamiento del flag recordOn está en la referencia del checkbox.
Caso C — Formulario público compartible (FORM_LINK)
Sin código. El dashboard genera una URL pública (https://app.dpolab.com/consent/<formKey>) que puedes compartir por:
- QR físico (cartel de videovigilancia, mostrador, factura)
- Botón en tu sitio que abre la URL
El titular accede, ve el formulario con los propósitos que definiste y los firma. El consentimiento queda con channel=FORM_LINK.
Para versión sandbox: agrega ?sandbox=1 a la URL. Funciona igual pero no cuenta en tus métricas.
Caso D — Webhook inbound desde un sistema externo
Cuando un CRM, e-commerce o CDP capta el consentimiento del titular (ej. Shopify Checkout), puede notificarlo a DPOLab vía webhook firmado.
Paso 1: crear el endpoint en el dashboard
Dashboard → CMP → tu aviso → Webhooks → Crear endpoint. El dashboard te devuelve dos valores que necesitas guardar:
endpointKey— UUID público que va en la URL del webhook.signingSecret— 256 bits hex. Se muestra una sola vez. Guárdalo en tu vault (1Password, AWS Secrets Manager, etc.). Si lo pierdes, hay que rotarlo desde el dashboard.
Paso 2: enviar el consentimiento desde tu backend
URL: POST https://api.dpolab.com/api/v1/webhooks/consent/{endpointKey}
Headers obligatorios:
Content-Type: application/jsonx-dpolab-signature: sha256=<hex>donde el<hex>es el HMAC SHA256 del body raw firmado con tusigningSecret.
Body:
{
"subject": { "email": "[email protected]", "ref": "external-id-001" },
"consents": [
{ "category": "privacy_policy", "granted": true }
],
"action": "GIVEN",
"metadata": { "source": "shopify-checkout", "order_id": 12345 }
}Ejemplo de firma en bash:
PAYLOAD='{"subject":{"email":"[email protected]","ref":"external-id-001"},"consents":[{"category":"privacy_policy","granted":true}],"action":"GIVEN"}'
SIGNATURE="sha256=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$SIGNING_SECRET" | sed 's/^.* //')"
curl -X POST "https://api.dpolab.com/api/v1/webhooks/consent/$ENDPOINT_KEY" \
-H "Content-Type: application/json" \
-H "x-dpolab-signature: $SIGNATURE" \
-d "$PAYLOAD"Respuestas esperadas:
202 Accepted— webhook recibido, el consentimiento se procesa de forma asíncrona (queda visible en el dashboard en 1-2 segundos).401 INVALID_SIGNATURE— la firma HMAC no coincide. Revisa secret y formato del body.404 NOT_FOUND— endpoint inactivo oendpointKeyincorrecto.
El body se debe enviar como JSON compacto (sin espacios ni saltos de línea entre los campos). Hoy el backend recalcula el HMAC sobre JSON.stringify(body) después de parsear, y si tu cliente serializa con formato (JSON.stringify(payload, null, 2) en Node, json.dumps(payload, indent=2) en Python), los hashes no van a coincidir y vas a recibir 401. Esta es una limitación conocida del backend y está en backlog para corregirse (firmar contra rawBody). Mientras tanto: serializa compacto.
Paso 3: mapeo de propósitos (opcional)
Si tu CRM usa nombres distintos para los propósitos (ej. tu CRM dice email_marketing y tu aviso usa comunicaciones), puedes configurar purpose_mapping en el endpoint para que se traduzcan automáticamente. Esto se hace desde el dashboard (próximamente con UI dedicada — hoy es JSONB crudo, contacta al equipo si lo necesitas).
Auditoría: verificar la cadena criptográfica
DPOLab encadena cada record de consentimiento con un hash SHA256 que incluye el hash del record anterior. Eso convierte la tabla de consentimientos en una cadena tipo blockchain: si alguien edita un record en la base de datos, la cadena se rompe y el dashboard lo detecta.
Para verificar:
Dashboard → CMP → tu aviso → tab Detalle (Informe).- Arriba de la lista de records, click "Verificar ahora".
- El reporte muestra:
- Cadena íntegra ✓ (verde): N records auditados, ningún hash roto.
- Cadena rota ✗ (rojo): cuántos eslabones fallaron y en qué posición. Si esto aparece sin que nadie haya modificado la BD, es bug — repórtalo.
Este reporte es la pieza que tu DPO puede presentar a un fiscalizador APDP como prueba de integridad de los consentimientos.
Troubleshooting
"El banner aparece pero al aceptar sale 400"
Causa típica: el SDK del navegador es una versión vieja cacheada. Haz un refresco completo con cache deshabilitado (DevTools → Network → "Disable cache" + Cmd+Shift+R) o cambia la URL del <script src> agregando ?v=2 para invalidar el cache.
"El snippet de producción da 403 CORS"
El dominio desde donde se sirve tu sitio no está registrado en Dashboard → Settings → Dominios o no está vinculado al aviso. Mientras lo configuras, usa el snippet sandbox.
"El checkbox graba consentimientos al marcarlo, no en el submit"
Estás usando el modo recordOn: 'change' (legacy). Quítalo del config — el default es 'submit' desde mayo 2026.
"El webhook devuelve 401 INVALID_SIGNATURE con la firma correcta"
Casi siempre es formato del JSON. Serializa el body compacto (sin espacios) tanto en la firma como en el curl. Ver el callout del Caso D.
"Verificar cadena reporta hash_mismatch en records antiguos"
Records creados antes de mayo 2026 tienen un bug histórico en el cálculo del timestamp del hash. Los records nuevos están bien. No es restaurable — esos quedan marcados como rotos para siempre. Si te molesta, borra los antiguos directamente en BD y vuelve a verificar.
"El visitante europeo ve el banner en español"
El SDK detecta navigator.language. Si tu base es internacional, agrega language: 'es' | 'en' explícito al init().