> ## 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.

# Entity Webhook Events

> Receive real-time notifications when entities are created, updated, or their status changes — with gu1 webhook events for real-time downstream integration.

## Overview

Entity webhook events allow you to receive real-time notifications when entities (persons, companies, devices, etc.) are created, updated, or their status changes in the Gu1 platform. These events enable you to keep your systems synchronized with Gu1 and automate workflows based on entity lifecycle changes.

## Why Use Entity Events?

<CardGroup cols={2}>
  <Card title="Real-Time Sync" icon="arrows-rotate">
    Keep your database synchronized with Gu1 entity data
  </Card>

  <Card title="Automated Workflows" icon="robot">
    Trigger actions when entity status changes
  </Card>

  <Card title="Audit Trail" icon="list-check">
    Track all entity changes for compliance
  </Card>

  <Card title="Efficient" icon="gauge-high">
    No need to poll the API for updates
  </Card>
</CardGroup>

## Available Events

### entity.created

Triggered when a new entity is created in Gu1.

**When it fires**:

* A new person, company, device, or other entity is created via `POST /entities`

**Filters available**:

* `entityTypes`: Only receive events for specific entity types (e.g., `["person", "company"]`)

### entity.updated

Triggered when an entity's data is updated (excluding status changes).

**When it fires**:

* Entity information is updated via `PATCH /entities/:id`
* Changes to name, attributes, entity data, tax ID, etc.

**Note**: Status changes trigger `entity.status_changed` instead.

**Filters available**:

* `entityTypes`: Only receive events for specific entity types

### entity.status\_changed

Triggered when an entity's status changes.

**When it fires**:

* Entity status transitions (e.g., `under_review` → `active`, `active` → `blocked`)
* Status updates via `PATCH /entities/:id` or automated compliance actions

**Filters available**:

* `entityTypes`: Filter by entity type
* `statusChanges.from`: Only trigger when changing FROM specific status
* `statusChanges.to`: Only trigger when changing TO specific status

<Note>
  Organizations explicitly configured to receive the legacy flat entity webhook also receive
  `riskScore` and `documentNumber` at the top level of the payload. These are additive fields;
  legacy status values such as `IN_PROGRESS` and `APPROVED` remain unchanged.
</Note>

### entity.country\_activation\_changed

Triggered when a merchant entity's **operational country activation** status changes.

**When it fires**:

* `PATCH /entities/:id/country-activations/:countryCode` with a new status (not idempotent repeats)

**Statuses**: `deactivated`, `activation_requested`, `activation_in_progress`, `activated`. Transitions are free-form.

**Note**: This is an operational flag only. Entity profile data is unchanged — re-fetch via `GET /entities/:id` if needed.

**Filters available**:

* `entityTypes`: Filter by entity type (e.g., `["company"]`)

## Event Payload Examples

### 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"
    }
  }
}
```

**Key Fields**:

* `entity`: Complete entity object with all data
* `entity.externalId`: Your unique identifier for the entity
* `entity.type`: Entity type (person, company, device, etc.)
* `entity.status`: Current status (under\_review, active, blocked, etc.)
* `createdBy`: User ID who created the entity
* `metadata`: Additional context about the creation

### 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"
  }
}
```

**Key Fields**:

* `entity`: Complete entity object with updated data
* `changes`: Object showing what changed (old vs new values)
* `updatedBy`: User ID who updated the entity
* `reason`: Optional reason for the update

### 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"
    }
  }
}
```

**Key Fields**:

* `status`: New status
* `previousStatus`: Previous status
* `reason`: Why the status changed
* `entity`: Complete entity object

### 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"
  }
}
```

**Key Fields**:

* `countryCode`: Country whose activation changed (AR, BR, CL, CO, MX, US)
* `status` / `previousStatus`: New and prior activation state
* `activeCountryCodes`: Snapshot of all `activated` countries after this change (allowlist order)
* `countries`: Full snapshot of all allowlist countries after this change
* `timeline`: Chronological status changes for this country (from audit trail), each with `previousStatus`, `status`, and `changedAt`
* `entity.externalId`: Your merchant identifier
* `changedAt`: ISO timestamp of the change

## Filter Configuration

### Filter by Entity Type

Only receive events for specific entity types:

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

This configuration will only trigger webhooks for person and company entities, ignoring devices and other types.

### Filter by Status Change

Only receive events when entity status changes to specific values:

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

This will only trigger when a person entity is changed **TO** `blocked` status.

**Filter when changing FROM a specific status**:

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

This will only trigger when status changes from `active` to `suspended`.

## Code Examples

### Node.js - Handling Entity Events

```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 {
    // Verify signature (see security guide)
    if (!verifySignature(req.rawBody, req.headers['x-webhook-signature'])) {
      return res.status(401).json({ error: 'Invalid signature' });
    }

    const { event, payload } = req.body;

    // Route to appropriate handler
    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})`);

  // Sync to your database
  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
    }
  });

  // Trigger workflows based on entity type
  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);

  // Update your database
  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()
    }
  });

  // Log changes for audit trail
  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}`);

  // Update database
  await db.entities.update({
    where: { gu1Id: entity.id },
    data: {
      status,
      statusChangedAt: new Date()
    }
  });

  // Take action based on new status
  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;
  }

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

app.listen(3000);
```

### Python - Handling Entity Events

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

app = Flask(__name__)

@app.route('/webhooks/entity', methods=['POST'])
def entity_webhook():
    try:
        # Verify signature (see security guide)
        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

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

        # Route to handler
        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']})")

    # Sync to database
    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
    })

    # Send welcome email for persons
    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']}")

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

    # Log changes
    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}")

    # Update database
    db.entities.update(
        {'gu1_id': entity['id']},
        {'status': status}
    )

    # Take action based on status
    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')

    # Log status change
    db.status_history.insert({
        'entity_id': entity['id'],
        'from_status': previous_status,
        'to_status': status,
        'reason': reason
    })

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

## Use Cases

### Use Case 1: Real-Time Database Sync

Keep your local database synchronized with 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()
    }
  });
}
```

### Use Case 2: Automated Account Activation

Automatically activate customer accounts when status changes to `active`:

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

  if (status === 'active' && previousStatus === 'under_review') {
    // Enable login
    await auth.enableUser(entity.externalId);

    // Grant access to services
    await services.grantAccess(entity.externalId);

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

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

### Use Case 3: Compliance Monitoring

Track and respond to entity status changes for compliance:

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

  // Alert compliance team when entity is blocked
  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()
      }
    });

    // Create case in compliance system
    await compliance.createCase({
      entityId: entity.id,
      type: 'blocked_entity',
      priority: 'high',
      data: entity
    });
  }
}
```

### Use Case 4: Customer Notifications

Notify customers when their information changes:

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

  // Check if email was updated
  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.'
    });

    // Also send to old email
    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.'
    });
  }

  // Notify on important field changes
  const importantFields = ['taxId', 'countryCode', 'name'];
  const importantChanges = Object.keys(changes).some(key =>
    importantFields.some(field => key.includes(field))
  );

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

## Best Practices

<AccordionGroup>
  <Accordion title="Use externalId for Lookups">
    The `entity.externalId` is your unique identifier. Use it to look up entities in your database:

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

  <Accordion title="Store Gu1 Entity ID">
    Always store the Gu1 entity ID in your database for reference:

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

  <Accordion title="Handle All Event Types">
    Even if you only subscribe to specific events, handle all event types gracefully:

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

      default:
        console.warn('Unhandled event type:', event);
        // Still return 200
    }
    ```
  </Accordion>

  <Accordion title="Log Changes for Audit">
    Keep an audit trail of all entity changes:

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

  <Accordion title="Implement Idempotency">
    Use the entity ID and timestamp to prevent duplicate processing:

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

    if (processed) {
      return; // Skip duplicate
    }
    ```
  </Accordion>

  <Accordion title="Use Filters to Reduce Noise">
    Configure filters to only receive relevant events:

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

## Troubleshooting

<AccordionGroup>
  <Accordion title="Not Receiving entity.created Events">
    **Check**:

    * Webhook is subscribed to `entity.created` event
    * Entity type matches your filters (if configured)
    * Webhook is enabled in dashboard
    * Endpoint is publicly accessible

    **Test**:

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

  <Accordion title="Missing Changes in entity.updated">
    The `changes` object only includes fields that actually changed. If you don't see a field, it means it wasn't updated.

    **Example**:

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

    Only `name` changed, other fields remain the same.
  </Accordion>

  <Accordion title="entity.status_changed Not Firing">
    **Check**:

    * Status actually changed (not just entity updated)
    * Filters match the status change (from/to)
    * Status change is not being filtered out

    **Example filter that might block events**:

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

    This will ONLY fire when status changes TO `blocked`.
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="KYC Events" icon="id-card" href="/en/webhooks/events/kyc-events">
    Handle KYC verification events
  </Card>

  <Card title="Rule Events" icon="gavel" href="/en/webhooks/events/rule-events">
    Process compliance rule triggers
  </Card>

  <Card title="Webhook Security" icon="shield" href="/en/webhooks/security">
    Secure your webhook endpoints
  </Card>

  <Card title="Configuration" icon="gear" href="/en/webhooks/configuration">
    Configure webhook settings
  </Card>
</CardGroup>
