Skip to main content

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?

Real-Time Sync

Keep your database synchronized with Gu1 entity data

Automated Workflows

Trigger actions when entity status changes

Audit Trail

Track all entity changes for compliance

Efficient

No need to poll the API for updates

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_reviewactive, activeblocked)
  • 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

Event Payload Examples

entity.created

{
  "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

{
  "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

{
  "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

Filter Configuration

Filter by Entity Type

Only receive events for specific entity types:
{
  "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:
{
  "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:
{
  "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

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

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:
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:
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:
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:
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

The entity.externalId is your unique identifier. Use it to look up entities in your database:
const customer = await db.customers.findUnique({
  where: { externalId: entity.externalId }
});
Always store the Gu1 entity ID in your database for reference:
await db.customers.create({
  data: {
    externalId: entity.externalId,
    gu1Id: entity.id, // Store this
    name: entity.name
  }
});
Even if you only subscribe to specific events, handle all event types gracefully:
switch (event) {
  case 'entity.created':
    await handleEntityCreated(payload);
    break;

  default:
    console.warn('Unhandled event type:', event);
    // Still return 200
}
Keep an audit trail of all entity changes:
await db.auditLog.create({
  data: {
    entityId: entity.id,
    event,
    changes,
    timestamp: new Date(),
    webhook: req.body
  }
});
Use the entity ID and timestamp to prevent duplicate processing:
const webhookId = `${entity.id}_${event}_${timestamp}`;
const processed = await db.webhookLog.findUnique({
  where: { webhookId }
});

if (processed) {
  return; // Skip duplicate
}
Configure filters to only receive relevant events:
{
  "eventTypes": ["entity.status_changed"],
  "filters": {
    "entityTypes": ["person"],
    "statusChanges": {
      "to": "active"
    }
  }
}

Troubleshooting

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:
# Create a test entity
curl -X POST https://api.gu1.io/entities \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{"name":"Test","type":"person"}'
The changes object only includes fields that actually changed. If you don’t see a field, it means it wasn’t updated.Example:
{
  "changes": {
    "name": {
      "old": "John",
      "new": "John Doe"
    }
  }
}
Only name changed, other fields remain the same.
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:
{
  "filters": {
    "statusChanges": {
      "to": "blocked"
    }
  }
}
This will ONLY fire when status changes TO blocked.

Next Steps