> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gu1.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Eventos de Webhook de Entidades

> Reciba notificaciones en tiempo real cuando se crean, actualizan entidades o cambia su estado. Consulta el esquema del request, códigos de respuesta.

## Descripción General

Los eventos de webhook de entidades le permiten recibir notificaciones en tiempo real cuando se crean, actualizan entidades (personas, empresas, dispositivos, etc.) o su estado cambia en la plataforma Gu1. Estos eventos le permiten mantener sus sistemas sincronizados con Gu1 y automatizar flujos de trabajo basados en cambios en el ciclo de vida de las entidades.

## ¿Por Qué Usar Eventos de Entidades?

<CardGroup cols={2}>
  <Card title="Sincronización en Tiempo Real" icon="arrows-rotate">
    Mantenga su base de datos sincronizada con los datos de entidades de Gu1
  </Card>

  <Card title="Flujos de Trabajo Automatizados" icon="robot">
    Active acciones cuando el estado de la entidad cambie
  </Card>

  <Card title="Registro de Auditoría" icon="list-check">
    Rastree todos los cambios de entidades para cumplimiento
  </Card>

  <Card title="Eficiente" icon="gauge-high">
    No es necesario consultar la API para actualizaciones
  </Card>
</CardGroup>

## Eventos Disponibles

### entity.created

Se activa cuando se crea una nueva entidad en Gu1.

**Cuándo se dispara**:

* Se crea una nueva persona, empresa, dispositivo u otra entidad a través de `POST /entities`

**Filtros disponibles**:

* `entityTypes`: Solo recibe eventos para tipos de entidades específicos (ej., `["person", "company"]`)

### entity.updated

Se activa cuando se actualizan los datos de una entidad (excluyendo cambios de estado).

**Cuándo se dispara**:

* Se actualiza la información de la entidad a través de `PATCH /entities/:id`
* Cambios en nombre, atributos, datos de entidad, ID fiscal, etc.

**Nota**: Los cambios de estado activan `entity.status_changed` en su lugar.

**Filtros disponibles**:

* `entityTypes`: Solo recibe eventos para tipos de entidades específicos

### entity.status\_changed

Se activa cuando cambia el estado de una entidad.

**Cuándo se dispara**:

* Transiciones de estado de entidad (ej., `under_review` → `active`, `active` → `blocked`)
* Actualizaciones de estado a través de `PATCH /entities/:id` o acciones de cumplimiento automatizadas

**Filtros disponibles**:

* `entityTypes`: Filtrar por tipo de entidad
* `statusChanges.from`: Solo activar cuando cambie DESDE un estado específico
* `statusChanges.to`: Solo activar cuando cambie A un estado específico

<Note>
  Las organizaciones configuradas explícitamente para recibir el webhook legacy plano de
  entidades también reciben `riskScore` y `documentNumber` en el nivel superior del payload.
  Son campos aditivos; los estados legacy como `IN_PROGRESS` y `APPROVED` no cambian.
</Note>

### entity.country\_activation\_changed

Se activa cuando cambia el **estado operativo de activación por país** de un merchant.

**Cuándo se dispara**:

* `PATCH /entities/:id/country-activations/:countryCode` con un estado nuevo (no en repeticiones idempotentes)

**Estados**: `deactivated`, `activation_requested`, `activation_in_progress`, `activated`. Las transiciones son libres.

**Nota**: Es solo un flag operativo. Los datos de la entidad no cambian — re-consultá con `GET /entities/:id` si hace falta.

**Filtros disponibles**:

* `entityTypes`: Filtrar por tipo de entidad (ej., `["company"]`)

## Ejemplos de Payload de Eventos

### entity.created

```json theme={null}
{
  "event": "entity.created",
  "timestamp": "2025-01-07T10:00:00.000Z",
  "payload": {
    "entity": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "externalId": "customer_abc123",
      "name": "John Doe",
      "type": "person",
      "taxId": "123-45-6789",
      "countryCode": "US",
      "status": "under_review",
      "isClient": false,
      "attributes": {
        "email": "john.doe@example.com",
        "phone": "+1234567890",
        "customerTier": "premium"
      },
      "entityData": {
        "person": {
          "firstName": "John",
          "lastName": "Doe",
          "dateOfBirth": "1990-01-15",
          "nationality": "US",
          "occupation": "Software Engineer"
        }
      },
      "createdAt": "2025-01-07T10:00:00.000Z",
      "updatedAt": "2025-01-07T10:00:00.000Z"
    },
    "createdBy": "user_xyz789",
    "metadata": {
      "source": "api",
      "userAgent": "Mozilla/5.0...",
      "ipAddress": "192.168.1.100"
    }
  }
}
```

**Campos Clave**:

* `entity`: Objeto de entidad completo con todos los datos
* `entity.externalId`: Su identificador único para la entidad
* `entity.type`: Tipo de entidad (person, company, device, etc.)
* `entity.status`: Estado actual (under\_review, active, blocked, etc.)
* `createdBy`: ID del usuario que creó la entidad
* `metadata`: Contexto adicional sobre la creación

### entity.updated

```json theme={null}
{
  "event": "entity.updated",
  "timestamp": "2025-01-07T10:05:00.000Z",
  "payload": {
    "entity": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "externalId": "customer_abc123",
      "name": "John Doe",
      "type": "person",
      "taxId": "123-45-6789",
      "countryCode": "US",
      "status": "active",
      "isClient": false,
      "attributes": {
        "email": "john.doe@newemail.com",
        "phone": "+1234567890",
        "customerTier": "premium"
      },
      "entityData": {
        "person": {
          "firstName": "John",
          "lastName": "Doe",
          "dateOfBirth": "1990-01-15",
          "nationality": "US",
          "occupation": "Senior Software Engineer"
        }
      },
      "updatedAt": "2025-01-07T10:05:00.000Z"
    },
    "changes": {
      "attributes.email": {
        "old": "john.doe@example.com",
        "new": "john.doe@newemail.com"
      },
      "entityData.person.occupation": {
        "old": "Software Engineer",
        "new": "Senior Software Engineer"
      }
    },
    "updatedBy": "user_xyz789",
    "reason": "Customer provided updated information"
  }
}
```

**Campos Clave**:

* `entity`: Objeto de entidad completo con datos actualizados
* `changes`: Objeto que muestra qué cambió (valores antiguos vs nuevos)
* `updatedBy`: ID del usuario que actualizó la entidad
* `reason`: Razón opcional para la actualización

### entity.status\_changed

```json theme={null}
{
  "event": "entity.status_changed",
  "timestamp": "2025-01-07T10:10:00.000Z",
  "payload": {
    "status": "active",
    "previousStatus": "under_review",
    "reason": "KYC verification completed successfully",
    "entity": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "externalId": "customer_abc123",
      "name": "John Doe",
      "type": "person",
      "taxId": "123-45-6789",
      "countryCode": "US",
      "status": "active",
      "updatedAt": "2025-01-07T10:10:00.000Z"
    }
  }
}
```

**Campos Clave**:

* `status`: Nuevo estado
* `previousStatus`: Estado anterior
* `reason`: Por qué cambió el estado
* `entity`: Objeto de entidad completo

### entity.country\_activation\_changed

```json theme={null}
{
  "event": "entity.country_activation_changed",
  "timestamp": "2026-07-03T15:00:00.000Z",
  "organizationId": "org-uuid",
  "payload": {
    "entity": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "externalId": "merchant_123",
      "name": "Acme Corp",
      "type": "company",
      "countryCode": "US"
    },
    "countryCode": "CO",
    "status": "activation_requested",
    "previousStatus": "deactivated",
    "activeCountryCodes": ["MX"],
    "countries": [
      { "countryCode": "AR", "status": "deactivated" },
      { "countryCode": "CO", "status": "activation_requested" },
      { "countryCode": "MX", "status": "activated" }
    ],
    "timeline": [
      {
        "previousStatus": "deactivated",
        "status": "activation_requested",
        "changedAt": "2026-07-03T15:00:00.000Z"
      }
    ],
    "changedAt": "2026-07-03T15:00:00.000Z"
  }
}
```

**Campos Clave**:

* `countryCode`: País cuya activación cambió (AR, BR, CL, CO, MX, US)
* `status` / `previousStatus`: Estado nuevo y anterior
* `activeCountryCodes`: Snapshot de países en `activated` tras el cambio (orden del allowlist)
* `countries`: Snapshot completo de los seis países del allowlist tras el cambio
* `timeline`: Historial cronológico de cambios de estado para ese país (desde auditoría), cada ítem con `previousStatus`, `status` y `changedAt`
* `entity.externalId`: Identificador del merchant
* `changedAt`: Timestamp ISO del cambio

## Configuración de Filtros

### Filtrar por Tipo de Entidad

Solo reciba eventos para tipos de entidades específicos:

```json theme={null}
{
  "eventTypes": ["entity.created", "entity.updated"],
  "filters": {
    "entityTypes": ["person", "company"]
  }
}
```

Esta configuración solo activará webhooks para entidades de persona y empresa, ignorando dispositivos y otros tipos.

### Filtrar por Cambio de Estado

Solo reciba eventos cuando el estado de la entidad cambie a valores específicos:

```json theme={null}
{
  "eventTypes": ["entity.status_changed"],
  "filters": {
    "entityTypes": ["person"],
    "statusChanges": {
      "to": "blocked"
    }
  }
}
```

Esto solo se activará cuando una entidad de persona sea cambiada **A** estado `blocked`.

**Filtrar cuando cambie DESDE un estado específico**:

```json theme={null}
{
  "eventTypes": ["entity.status_changed"],
  "filters": {
    "statusChanges": {
      "from": "active",
      "to": "suspended"
    }
  }
}
```

Esto solo se activará cuando el estado cambie de `active` a `suspended`.

## Ejemplos de Código

### Node.js - Manejo de Eventos de Entidades

```javascript theme={null}
const express = require('express');
const app = express();

app.use(express.json({
  verify: (req, res, buf) => {
    req.rawBody = buf.toString('utf8');
  }
}));

app.post('/webhooks/entity', async (req, res) => {
  try {
    // Verificar firma (ver guía de seguridad)
    if (!verifySignature(req.rawBody, req.headers['x-webhook-signature'])) {
      return res.status(401).json({ error: 'Invalid signature' });
    }

    const { event, payload } = req.body;

    // Redirigir al controlador apropiado
    switch (event) {
      case 'entity.created':
        await handleEntityCreated(payload);
        break;

      case 'entity.updated':
        await handleEntityUpdated(payload);
        break;

      case 'entity.status_changed':
        await handleEntityStatusChanged(payload);
        break;

      default:
        console.warn('Unknown event type:', event);
    }

    res.status(200).json({ success: true });
  } catch (error) {
    console.error('Webhook error:', error);
    res.status(500).json({ error: error.message });
  }
});

async function handleEntityCreated(payload) {
  const { entity, createdBy, metadata } = payload;

  console.log(`Entity created: ${entity.name} (${entity.type})`);

  // Sincronizar con su base de datos
  await db.entities.create({
    data: {
      gu1Id: entity.id,
      externalId: entity.externalId,
      name: entity.name,
      type: entity.type,
      status: entity.status,
      data: entity.entityData,
      attributes: entity.attributes,
      createdBy,
      metadata
    }
  });

  // Activar flujos de trabajo basados en tipo de entidad
  if (entity.type === 'person') {
    await sendWelcomeEmail(entity.attributes.email);
  }
}

async function handleEntityUpdated(payload) {
  const { entity, changes, updatedBy, reason } = payload;

  console.log(`Entity updated: ${entity.name}`);
  console.log('Changes:', changes);

  // Actualizar su base de datos
  await db.entities.update({
    where: { gu1Id: entity.id },
    data: {
      name: entity.name,
      status: entity.status,
      data: entity.entityData,
      attributes: entity.attributes,
      lastUpdatedBy: updatedBy,
      updatedAt: new Date()
    }
  });

  // Registrar cambios para registro de auditoría
  await db.entityChanges.create({
    data: {
      entityId: entity.id,
      changes,
      changedBy: updatedBy,
      reason,
      timestamp: new Date()
    }
  });
}

async function handleEntityStatusChanged(payload) {
  const { entity, status, previousStatus, reason } = payload;

  console.log(`Entity status changed: ${previousStatus} → ${status}`);

  // Actualizar base de datos
  await db.entities.update({
    where: { gu1Id: entity.id },
    data: {
      status,
      statusChangedAt: new Date()
    }
  });

  // Tomar acción basada en nuevo estado
  switch (status) {
    case 'active':
      await activateCustomerAccount(entity.externalId);
      await sendNotification(entity, 'Your account has been activated');
      break;

    case 'blocked':
      await deactivateCustomerAccount(entity.externalId);
      await sendNotification(entity, 'Your account has been blocked');
      break;

    case 'suspended':
      await suspendCustomerAccount(entity.externalId);
      await sendNotification(entity, 'Your account has been suspended');
      break;
  }

  // Registrar cambio de estado
  await db.statusHistory.create({
    data: {
      entityId: entity.id,
      fromStatus: previousStatus,
      toStatus: status,
      reason,
      timestamp: new Date()
    }
  });
}

app.listen(3000);
```

### Python - Manejo de Eventos de Entidades

```python theme={null}
from flask import Flask, request, jsonify
import logging

app = Flask(__name__)

@app.route('/webhooks/entity', methods=['POST'])
def entity_webhook():
    try:
        # Verificar firma (ver guía de seguridad)
        signature = request.headers.get('X-Webhook-Signature')
        raw_body = request.get_data(as_text=True)

        if not verify_signature(raw_body, signature):
            return jsonify({'error': 'Invalid signature'}), 401

        # Analizar webhook
        payload = request.json
        event = payload['event']
        data = payload['payload']

        # Redirigir al controlador
        handlers = {
            'entity.created': handle_entity_created,
            'entity.updated': handle_entity_updated,
            'entity.status_changed': handle_entity_status_changed
        }

        handler = handlers.get(event)
        if handler:
            handler(data)
        else:
            logging.warning(f'Unknown event type: {event}')

        return jsonify({'success': True}), 200

    except Exception as e:
        logging.error(f'Webhook error: {e}')
        return jsonify({'error': str(e)}), 500

def handle_entity_created(data):
    entity = data['entity']
    created_by = data['createdBy']

    logging.info(f"Entity created: {entity['name']} ({entity['type']})")

    # Sincronizar con base de datos
    db.entities.insert({
        'gu1_id': entity['id'],
        'external_id': entity['externalId'],
        'name': entity['name'],
        'type': entity['type'],
        'status': entity['status'],
        'data': entity['entityData'],
        'attributes': entity['attributes'],
        'created_by': created_by
    })

    # Enviar correo de bienvenida para personas
    if entity['type'] == 'person' and 'email' in entity['attributes']:
        send_welcome_email(entity['attributes']['email'])

def handle_entity_updated(data):
    entity = data['entity']
    changes = data['changes']
    updated_by = data.get('updatedBy')
    reason = data.get('reason')

    logging.info(f"Entity updated: {entity['name']}")

    # Actualizar base de datos
    db.entities.update(
        {'gu1_id': entity['id']},
        {
            'name': entity['name'],
            'status': entity['status'],
            'data': entity['entityData'],
            'attributes': entity['attributes'],
            'last_updated_by': updated_by
        }
    )

    # Registrar cambios
    db.entity_changes.insert({
        'entity_id': entity['id'],
        'changes': changes,
        'changed_by': updated_by,
        'reason': reason
    })

def handle_entity_status_changed(data):
    entity = data['entity']
    status = data['status']
    previous_status = data['previousStatus']
    reason = data.get('reason')

    logging.info(f"Status changed: {previous_status} → {status}")

    # Actualizar base de datos
    db.entities.update(
        {'gu1_id': entity['id']},
        {'status': status}
    )

    # Tomar acción basada en estado
    if status == 'active':
        activate_customer_account(entity['externalId'])
        send_notification(entity, 'Your account has been activated')
    elif status == 'blocked':
        deactivate_customer_account(entity['externalId'])
        send_notification(entity, 'Your account has been blocked')

    # Registrar cambio de estado
    db.status_history.insert({
        'entity_id': entity['id'],
        'from_status': previous_status,
        'to_status': status,
        'reason': reason
    })

if __name__ == '__main__':
    app.run(port=3000)
```

## Casos de Uso

### Caso de Uso 1: Sincronización de Base de Datos en Tiempo Real

Mantenga su base de datos local sincronizada con Gu1:

```javascript theme={null}
async function syncEntityToDatabase(entity) {
  await db.customers.upsert({
    where: { gu1Id: entity.id },
    update: {
      name: entity.name,
      status: entity.status,
      data: entity.entityData,
      attributes: entity.attributes,
      syncedAt: new Date()
    },
    create: {
      gu1Id: entity.id,
      externalId: entity.externalId,
      name: entity.name,
      type: entity.type,
      status: entity.status,
      data: entity.entityData,
      attributes: entity.attributes,
      syncedAt: new Date()
    }
  });
}
```

### Caso de Uso 2: Activación Automática de Cuenta

Active automáticamente cuentas de clientes cuando el estado cambie a `active`:

```javascript theme={null}
async function handleEntityStatusChanged(payload) {
  const { entity, status, previousStatus } = payload;

  if (status === 'active' && previousStatus === 'under_review') {
    // Habilitar inicio de sesión
    await auth.enableUser(entity.externalId);

    // Otorgar acceso a servicios
    await services.grantAccess(entity.externalId);

    // Enviar correo de bienvenida
    await email.send({
      to: entity.attributes.email,
      subject: 'Welcome! Your account is now active',
      template: 'account-activated',
      data: { name: entity.name }
    });

    // Registrar evento
    console.log(`Account activated: ${entity.name}`);
  }
}
```

### Caso de Uso 3: Monitoreo de Cumplimiento

Rastree y responda a cambios de estado de entidades para cumplimiento:

```javascript theme={null}
async function handleEntityStatusChanged(payload) {
  const { entity, status, previousStatus, reason } = payload;

  // Alertar al equipo de cumplimiento cuando una entidad es bloqueada
  if (status === 'blocked') {
    await slack.send({
      channel: '#compliance-alerts',
      message: `🚨 Entity blocked: ${entity.name} (${entity.externalId})`,
      fields: {
        'Entity ID': entity.id,
        'Previous Status': previousStatus,
        'Reason': reason,
        'Blocked At': new Date().toISOString()
      }
    });

    // Crear caso en sistema de cumplimiento
    await compliance.createCase({
      entityId: entity.id,
      type: 'blocked_entity',
      priority: 'high',
      data: entity
    });
  }
}
```

### Caso de Uso 4: Notificaciones a Clientes

Notifique a los clientes cuando su información cambie:

```javascript theme={null}
async function handleEntityUpdated(payload) {
  const { entity, changes } = payload;

  // Verificar si se actualizó el correo
  if (changes['attributes.email']) {
    await sendEmail({
      to: changes['attributes.email'].new,
      subject: 'Email address updated',
      body: 'Your email address has been updated. If this was not you, please contact support.'
    });

    // También enviar al correo antiguo
    await sendEmail({
      to: changes['attributes.email'].old,
      subject: 'Email address changed',
      body: 'Your email address has been changed. If this was not you, please contact support immediately.'
    });
  }

  // Notificar en cambios de campos importantes
  const importantFields = ['taxId', 'countryCode', 'name'];
  const importantChanges = Object.keys(changes).some(key =>
    importantFields.some(field => key.includes(field))
  );

  if (importantChanges) {
    await sendSecurityAlert(entity, changes);
  }
}
```

## Mejores Prácticas

<AccordionGroup>
  <Accordion title="Use externalId para Búsquedas">
    El `entity.externalId` es su identificador único. Úselo para buscar entidades en su base de datos:

    ```javascript theme={null}
    const customer = await db.customers.findUnique({
      where: { externalId: entity.externalId }
    });
    ```
  </Accordion>

  <Accordion title="Almacene el ID de Entidad de Gu1">
    Siempre almacene el ID de entidad de Gu1 en su base de datos para referencia:

    ```javascript theme={null}
    await db.customers.create({
      data: {
        externalId: entity.externalId,
        gu1Id: entity.id, // Almacenar esto
        name: entity.name
      }
    });
    ```
  </Accordion>

  <Accordion title="Maneje Todos los Tipos de Eventos">
    Incluso si solo se suscribe a eventos específicos, maneje todos los tipos de eventos con gracia:

    ```javascript theme={null}
    switch (event) {
      case 'entity.created':
        await handleEntityCreated(payload);
        break;

      default:
        console.warn('Unhandled event type:', event);
        // Aún así devolver 200
    }
    ```
  </Accordion>

  <Accordion title="Registre Cambios para Auditoría">
    Mantenga un registro de auditoría de todos los cambios de entidades:

    ```javascript theme={null}
    await db.auditLog.create({
      data: {
        entityId: entity.id,
        event,
        changes,
        timestamp: new Date(),
        webhook: req.body
      }
    });
    ```
  </Accordion>

  <Accordion title="Implemente Idempotencia">
    Use el ID de entidad y timestamp para prevenir procesamiento duplicado:

    ```javascript theme={null}
    const webhookId = `${entity.id}_${event}_${timestamp}`;
    const processed = await db.webhookLog.findUnique({
      where: { webhookId }
    });

    if (processed) {
      return; // Omitir duplicado
    }
    ```
  </Accordion>

  <Accordion title="Use Filtros para Reducir Ruido">
    Configure filtros para recibir solo eventos relevantes:

    ```json theme={null}
    {
      "eventTypes": ["entity.status_changed"],
      "filters": {
        "entityTypes": ["person"],
        "statusChanges": {
          "to": "active"
        }
      }
    }
    ```
  </Accordion>
</AccordionGroup>

## Solución de Problemas

<AccordionGroup>
  <Accordion title="No Recibo Eventos entity.created">
    **Verificar**:

    * El webhook está suscrito al evento `entity.created`
    * El tipo de entidad coincide con sus filtros (si están configurados)
    * El webhook está habilitado en el dashboard
    * El endpoint es públicamente accesible

    **Probar**:

    ```bash theme={null}
    # Crear una entidad de prueba
    curl -X POST https://api.gu1.io/entities \
      -H "Authorization: Bearer YOUR_TOKEN" \
      -d '{"name":"Test","type":"person"}'
    ```
  </Accordion>

  <Accordion title="Faltan Cambios en entity.updated">
    El objeto `changes` solo incluye campos que realmente cambiaron. Si no ve un campo, significa que no se actualizó.

    **Ejemplo**:

    ```json theme={null}
    {
      "changes": {
        "name": {
          "old": "John",
          "new": "John Doe"
        }
      }
    }
    ```

    Solo cambió `name`, otros campos permanecen igual.
  </Accordion>

  <Accordion title="entity.status_changed No Se Dispara">
    **Verificar**:

    * El estado realmente cambió (no solo se actualizó la entidad)
    * Los filtros coinciden con el cambio de estado (from/to)
    * El cambio de estado no está siendo filtrado

    **Ejemplo de filtro que podría bloquear eventos**:

    ```json theme={null}
    {
      "filters": {
        "statusChanges": {
          "to": "blocked"
        }
      }
    }
    ```

    Esto SOLO se disparará cuando el estado cambie A `blocked`.
  </Accordion>
</AccordionGroup>

## Próximos Pasos

<CardGroup cols={2}>
  <Card title="Eventos KYC" icon="id-card" href="/es/webhooks/events/kyc-events">
    Maneje eventos de verificación KYC
  </Card>

  <Card title="Eventos de Reglas" icon="gavel" href="/es/webhooks/events/rule-events">
    Procese activaciones de reglas de cumplimiento
  </Card>

  <Card title="Seguridad de Webhooks" icon="shield" href="/es/webhooks/security">
    Asegure sus endpoints de webhook
  </Card>

  <Card title="Configuración" icon="gear" href="/es/webhooks/configuration">
    Configure ajustes de webhook
  </Card>
</CardGroup>
