Skip to main content

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.

Overview

KYC (Know Your Customer) webhook events allow you to receive real-time notifications when a KYC verification status changes. Gu1 automatically sends HTTP POST requests to your configured webhook endpoint whenever a validation status updates, enabling you to automate customer onboarding workflows and maintain compliance.

Why Use KYC Webhooks?

Real-Time Updates

Get instant notifications when verification status changes

Efficient

No need to poll the API repeatedly

Automated Workflows

Automatically update user accounts based on verification results

Better UX

Notify customers immediately after verification

Available Events

Gu1 sends webhooks for the following KYC validation events:
Event TypeDescriptionWhen Triggered
kyc.validation_createdValidation session createdWhen you create a new KYC validation
kyc.validation_in_progressCustomer started verificationCustomer is actively completing verification (filling out form)
kyc.validation_in_reviewVerification under reviewVerification completed, requires manual review from compliance team
kyc.validation_approvedVerification approvedIdentity successfully verified
kyc.validation_rejectedVerification rejectedIdentity verification failed
kyc.validation_abandonedCustomer abandoned processCustomer left without completing
kyc.validation_expiredValidation session expiredSession expired (typically after 7 days)
kyc.validation_cancelledValidation cancelledValidation manually cancelled by organization

Event Payload Structure

Every KYC webhook uses the same outer envelope. The inner payload object is the KYC validation row as stored in Gu1 (same field names as in the database/API: id is the validation UUID, plus entityId, organizationId, status, decision, extractedData, verifiedFields, warnings, metadata, timestamps, etc.).
entity (full GΓΌeno entity row) is sent only for terminal outcomes: kyc.validation_approved and kyc.validation_rejected. It is not included for kyc.validation_created, in_progress, in_review, abandoned, expired, or cancelled. This is an additive field: all previous payload fields are unchanged. For kyc.validation_approved, optional person auto-population from KYC still runs after the webhook (same order as before); payload.entity is a snapshot at webhook send time (before that step). To read the entity after auto-population, call the Entities API.
{
  "event": "kyc.validation_approved",
  "timestamp": "2025-01-07T11:00:00Z",
  "organizationId": "org-uuid",
  "payload": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "entityId": "123e4567-e89b-12d3-a456-426614174000",
    "organizationId": "org-uuid",
    "status": "approved",
    "decision": {},
    "extractedData": {},
    "verifiedFields": [],
    "warnings": [],
    "entity": {
      "id": "123e4567-e89b-12d3-a456-426614174000",
      "externalId": "customer_xyz789",
      "name": "John Doe",
      "type": "person",
      "taxId": "…",
      "countryCode": "US",
      "status": "active",
      "entityData": {},
      "attributes": {}
    }
  }
}
When present, payload.entity includes all entity columns, JSON-serializable, with dates as ISO strings (entityData, attributes, email, phone, etc.).

Common Payload Fields

event
string
The event type (e.g., kyc.validation_approved)
timestamp
string
ISO 8601 timestamp when the event occurred
organizationId
string
Your organization ID
payload.id
string
The KYC validation ID in Gu1 (primary key of the validation row)
payload.entityId
string
The entity (person) ID being verified
payload.entity
object
Present only for kyc.validation_approved and kyc.validation_rejected. Snapshot of the entity row in GΓΌeno at webhook send time (on approve, before optional auto-population runs).
payload.status
string
Current validation status: pending, in_progress, in_review, approved, rejected, abandoned, expired, cancelled

Event-Specific Payloads

kyc.validation_created

Sent when a new KYC validation is created. The payload is the validation row; entity is not included (non-terminal event).
{
  "event": "kyc.validation_created",
  "timestamp": "2025-01-07T10:30:00Z",
  "organizationId": "org-123",
  "payload": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "entityId": "123e4567-e89b-12d3-a456-426614174000",
    "status": "pending",
    "providerSessionUrl": "https://kyc.gu1.io/validate/abc123",
    "expiresAt": "2025-01-14T10:30:00Z"
  }
}
Use case: Send the validation URL to your customer via email or SMS.

kyc.validation_in_progress

Sent when a customer starts the verification process. entity is not included.
{
  "event": "kyc.validation_in_progress",
  "timestamp": "2025-01-07T10:35:00Z",
  "organizationId": "org-123",
  "payload": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "entityId": "123e4567-e89b-12d3-a456-426614174000",
    "status": "in_progress"
  }
}
Use case: Update UI to show β€œVerification in progress” status.

kyc.validation_in_review

Sent when a customer completes verification and requires manual review from the compliance team. entity is not included.
{
  "event": "kyc.validation_in_review",
  "timestamp": "2025-01-07T10:50:00Z",
  "organizationId": "org-123",
  "payload": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "entityId": "123e4567-e89b-12d3-a456-426614174000",
    "status": "in_review"
  }
}
Use case: Notify compliance team for manual review. Update UI to show β€œUnder review by compliance team”.

kyc.validation_approved

Sent when verification is successfully completed. entity is included (full row at send time; optional auto-population may run afterward, same as before).
{
  "event": "kyc.validation_approved",
  "timestamp": "2025-01-07T11:00:00Z",
  "organizationId": "org-123",
  "payload": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "entityId": "123e4567-e89b-12d3-a456-426614174000",
    "entity": {
      "id": "123e4567-e89b-12d3-a456-426614174000",
      "externalId": "customer_xyz789",
      "name": "John Doe",
      "type": "person",
      "entityData": { "person": { "firstName": "John", "lastName": "Doe" } },
      "attributes": {}
    },
    "status": "approved",
    "verifiedAt": "2025-01-07T11:00:00Z",
    "extractedData": {
      "firstName": "John",
      "lastName": "Doe",
      "dateOfBirth": "1990-05-20",
      "nationality": "US",
      "documentNumber": "AB123456",
      "documentType": "passport",
      "documentExpiry": "2030-05-20",
      "address": {
        "street": "123 Main St",
        "city": "New York",
        "state": "NY",
        "postalCode": "10001",
        "country": "US"
      }
    },
    "verifiedFields": [
      "firstName",
      "lastName",
      "dateOfBirth",
      "nationality",
      "documentNumber"
    ],
    "warnings": [],
    "decision": {
      "id_verification": "pass",
      "face_match": "pass",
      "liveness": "pass",
      "document_authenticity": "pass"
    }
  }
}
Additional Fields:
  • entity: Full GΓΌeno entity record at send time (all columns; dates as ISO strings)
  • verifiedAt: Timestamp when verification was approved
  • extractedData: Personal information extracted from the document
  • verifiedFields: Array of fields that were successfully verified
  • warnings: Array of any warnings detected during verification (empty if approved)
  • decision: Verification results for each check
Use case: Activate customer account and grant access to services.

kyc.validation_rejected

Sent when verification fails. entity is included (current entity row in GΓΌeno; typically unchanged by KYC on rejection).
{
  "event": "kyc.validation_rejected",
  "timestamp": "2025-01-07T11:00:00Z",
  "organizationId": "org-123",
  "payload": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "entityId": "123e4567-e89b-12d3-a456-426614174000",
    "entity": {
      "id": "123e4567-e89b-12d3-a456-426614174000",
      "externalId": "customer_xyz789",
      "name": "John Doe",
      "type": "person",
      "entityData": {},
      "attributes": {}
    },
    "status": "rejected",
    "verifiedAt": "2025-01-07T11:00:00Z",
    "extractedData": {},
    "verifiedFields": [],
    "warnings": [
      "Document authenticity check failed",
      "Face match confidence low",
      "Liveness detection failed"
    ],
    "decision": {
      "id_verification": "fail",
      "face_match": "fail",
      "liveness": "fail",
      "document_authenticity": "fail"
    },
    "rejectionReason": "Document authenticity could not be verified"
  }
}
Additional Fields:
  • entity: Full GΓΌeno entity record at send time
  • warnings: Array of reasons why verification failed
  • rejectionReason: Primary reason for rejection
Use case: Notify customer that verification failed and provide guidance on next steps.

kyc.validation_abandoned

Sent when a customer starts but doesn’t complete the verification. entity is not included.
{
  "event": "kyc.validation_abandoned",
  "timestamp": "2025-01-07T10:45:00Z",
  "organizationId": "org-123",
  "payload": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "entityId": "123e4567-e89b-12d3-a456-426614174000",
    "status": "abandoned",
    "abandonedAt": "2025-01-07T10:45:00Z",
    "lastStep": "document_upload"
  }
}
Additional Fields:
  • lastStep: The last step the customer completed before abandoning
Use case: Send a reminder email to complete verification.

kyc.validation_expired

Sent when a validation session expires without completion. entity is not included.
{
  "event": "kyc.validation_expired",
  "timestamp": "2025-01-14T10:30:00Z",
  "organizationId": "org-123",
  "payload": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "entityId": "123e4567-e89b-12d3-a456-426614174000",
    "status": "expired",
    "expiresAt": "2025-01-14T10:30:00Z",
    "createdAt": "2025-01-07T10:30:00Z"
  }
}
Use case: Clean up pending validations and notify customer to restart verification.

kyc.validation_cancelled

Sent when a validation is manually cancelled by the organization. entity is not included.
{
  "event": "kyc.validation_cancelled",
  "timestamp": "2025-01-07T12:00:00Z",
  "organizationId": "org-123",
  "payload": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "entityId": "123e4567-e89b-12d3-a456-426614174000",
    "status": "cancelled",
    "cancelledAt": "2025-01-07T12:00:00Z",
    "cancelledBy": "user_123"
  }
}
Use case: Notify customer that validation was cancelled. Clean up associated resources and update status in your system.

Code Examples

Node.js - Handling KYC Events

const express = require('express');
const crypto = require('crypto');

const app = express();

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

app.post('/webhooks/kyc', async (req, res) => {
  try {
    // Verify webhook signature (see security guide)
    const signature = req.headers['x-webhook-signature'];
    const webhookSecret = process.env.GU1_WEBHOOK_SECRET;

    if (!verifySignature(req.rawBody, signature, webhookSecret)) {
      console.error('Invalid webhook signature');
      return res.status(401).json({ error: 'Invalid signature' });
    }

    // Extract webhook data
    const { event, timestamp, organizationId, payload } = req.body;

    console.log('Received KYC webhook:', {
      event,
      validationId: payload.id,
      status: payload.status
    });

    // Process the webhook based on event type
    await handleKycWebhook(event, payload);

    // Return 200 to acknowledge receipt
    res.status(200).json({
      success: true,
      message: 'Webhook received'
    });
  } catch (error) {
    console.error('Webhook error:', error);
    res.status(500).json({
      error: error.message
    });
  }
});

async function handleKycWebhook(event, data) {
  const validationId = data.id;
  const { entityId, entity, status } = data;

  // `entity` is only present for kyc.validation_approved / kyc.validation_rejected

  switch (event) {
    case 'kyc.validation_created':
      console.log('KYC validation created:', validationId, entityId);
      await sendSessionLinkToCustomer(entityId, data.providerSessionUrl);
      break;

    case 'kyc.validation_in_progress':
      await notifyCustomerByEntityId(entityId, 'verification-started');
      break;

    case 'kyc.validation_in_review':
      await notifyComplianceTeam(entityId, validationId);
      await notifyCustomerByEntityId(entityId, 'verification-in-review');
      break;

    case 'kyc.validation_approved': {
      if (!entity) break;
      const { extractedData, verifiedFields } = data;
      await db.updateEntity(entity.externalId, {
        kycValidationId: validationId,
        kycStatus: status,
        verifiedData: extractedData,
        verifiedFields,
        verifiedAt: data.verifiedAt,
        isVerified: true,
        lastUpdated: new Date()
      });
      await activateCustomerAccount(entity.externalId);
      await notifyCustomer(entity.externalId, 'verification-approved');
      break;
    }

    case 'kyc.validation_rejected': {
      if (!entity) break;
      await db.updateEntity(entity.externalId, {
        kycValidationId: validationId,
        kycStatus: status,
        isVerified: false,
        rejectionReasons: data.warnings,
        rejectionReason: data.rejectionReason,
        lastUpdated: new Date()
      });
      await notifyCustomer(entity.externalId, 'verification-rejected', {
        reasons: data.warnings
      });
      break;
    }

    case 'kyc.validation_abandoned':
      await notifyCustomerByEntityId(entityId, 'verification-incomplete', {
        lastStep: data.lastStep
      });
      break;

    case 'kyc.validation_expired':
      await notifyCustomerByEntityId(entityId, 'verification-expired');
      await db.deleteValidation(validationId);
      break;

    case 'kyc.validation_cancelled':
      await notifyCustomerByEntityId(entityId, 'verification-cancelled');
      await db.deleteValidation(validationId);
      break;
  }
}

function verifySignature(rawBody, signature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  return signature === expectedSignature;
}

app.listen(3000);

Python - Handling KYC Events

from flask import Flask, request, jsonify
import hmac
import hashlib
import logging

app = Flask(__name__)

@app.route('/webhooks/kyc', methods=['POST'])
def kyc_webhook():
    try:
        # Get signature from header
        signature = request.headers.get('X-Webhook-Signature')
        webhook_secret = os.getenv('GU1_WEBHOOK_SECRET')

        # Get raw body for signature verification
        raw_body = request.get_data(as_text=True)

        # Verify signature
        if not verify_signature(raw_body, signature, webhook_secret):
            logging.error('Invalid webhook signature')
            return jsonify({'error': 'Invalid signature'}), 401

        # Parse payload
        payload = request.json
        event = payload.get('event')
        data = payload.get('payload')

        logging.info(f'Received KYC webhook: {event}')

        # Process the webhook
        handle_kyc_webhook(event, data)

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

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

def verify_signature(raw_body, signature, secret):
    """Verify HMAC SHA-256 signature"""
    expected_signature = hmac.new(
        secret.encode('utf-8'),
        raw_body.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(signature, expected_signature)

def handle_kyc_webhook(event, data):
    validation_id = data['id']
    entity_id = data['entityId']
    entity = data.get('entity')  # only for approved / rejected
    status = data.get('status')

    if event == 'kyc.validation_created':
        send_session_link_to_customer(entity_id, data.get('providerSessionUrl'))

    elif event == 'kyc.validation_in_review':
        notify_compliance_team(entity_id, validation_id)
        notify_customer_by_entity_id(entity_id, 'verification-in-review')

    elif event == 'kyc.validation_approved':
        if not entity:
            return
        db.update_entity(
            external_id=entity['externalId'],
            kyc_validation_id=validation_id,
            kyc_status=status,
            verified_data=data.get('extractedData'),
            verified_fields=data.get('verifiedFields'),
            verified_at=data.get('verifiedAt'),
            is_verified=True,
            last_updated=datetime.now()
        )
        activate_customer_account(entity['externalId'])
        notify_customer(entity['externalId'], 'verification-approved')

    elif event == 'kyc.validation_rejected':
        if not entity:
            return
        db.update_entity(
            external_id=entity['externalId'],
            kyc_validation_id=validation_id,
            kyc_status=status,
            is_verified=False,
            rejection_reasons=data.get('warnings'),
            last_updated=datetime.now()
        )
        notify_customer(entity['externalId'], 'verification-rejected')

    elif event == 'kyc.validation_abandoned':
        notify_customer_by_entity_id(entity_id, 'verification-incomplete')

    elif event == 'kyc.validation_expired':
        notify_customer_by_entity_id(entity_id, 'verification-expired')
        db.delete_validation(validation_id)

    elif event == 'kyc.validation_cancelled':
        notify_customer_by_entity_id(entity_id, 'verification-cancelled')
        db.delete_validation(validation_id)

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

Use Cases

Use Case 1: Automatic Account Activation

Automatically activate customer accounts when KYC is approved:
async function handleKycApproved(data) {
  const { entity, extractedData, verifiedFields } = data;

  // Update customer record
  await db.customers.update({
    where: { externalId: entity.externalId },
    data: {
      kycStatus: 'approved',
      verifiedAt: new Date(),
      verifiedData: extractedData,
      isActive: true
    }
  });

  // Enable login
  await auth.enableUser(entity.externalId);

  // Grant access to services
  await services.grantAccess(entity.externalId, ['trading', 'withdrawal']);

  // Send welcome email
  await email.send({
    to: extractedData.email || entity.attributes?.email,
    subject: 'Welcome! Your account is verified',
    template: 'kyc-approved',
    data: {
      name: entity.name,
      verifiedAt: new Date().toISOString()
    }
  });

  // Log event
  await audit.log({
    action: 'kyc_approved',
    entityId: entity.id,
    timestamp: new Date()
  });
}

Use Case 2: Compliance Monitoring

Track KYC rejections for compliance review:
async function handleKycRejected(data) {
  const { entity, warnings, rejectionReason } = data;

  // Log rejection
  await compliance.logRejection({
    entityId: entity.id,
    externalId: entity.externalId,
    reasons: warnings,
    primaryReason: rejectionReason,
    timestamp: new Date()
  });

  // Alert compliance team if multiple rejections
  const rejectCount = await db.rejections.count({
    where: { entityId: entity.id }
  });

  if (rejectCount >= 3) {
    await slack.send({
      channel: '#compliance-alerts',
      message: `🚨 Multiple KYC rejections for ${entity.name} (${entity.externalId})`,
      fields: {
        'Entity ID': entity.id,
        'Rejection Count': rejectCount,
        'Latest Reason': rejectionReason
      }
    });
  }

  // Notify customer with guidance
  await email.send({
    to: entity.attributes?.email,
    subject: 'Verification unsuccessful',
    template: 'kyc-rejected',
    data: {
      name: entity.name,
      reasons: warnings,
      supportUrl: 'https://support.example.com/kyc'
    }
  });
}

Use Case 3: Abandoned Verification Recovery

Send reminders to customers who abandon verification:
async function handleKycAbandoned(data) {
  const { entityId, id: validationId, lastStep, providerSessionUrl } = data;

  // Schedule reminder email (resolve contact via entityId in your DB)
  await queue.scheduleEmail({
    to: await resolveCustomerEmail(entityId),
    subject: 'Complete your verification',
    template: 'kyc-reminder',
    data: {
      lastStep,
      validationUrl: providerSessionUrl
    },
    sendAt: new Date(Date.now() + 24 * 60 * 60 * 1000) // Send in 24 hours
  });

  await analytics.track({
    event: 'kyc_abandoned',
    entityId,
    validationId,
    lastStep,
    timestamp: new Date()
  });
}

Best Practices

For kyc.validation_approved and kyc.validation_rejected, payload.entity includes the full entity rowβ€”use entity.externalId to match the record you created in GΓΌeno.
const customer = await db.findCustomer({
  externalId: webhook.payload.entity.externalId
});
For other KYC events, use payload.entityId (and your own mapping) until you receive a terminal event.
Store the validation UUID from Gu1 (payload.id) in your database so you can query validation details later.
await db.updateCustomer(customer.id, {
  kycValidationId: payload.id,
  kycStatus: payload.status
});
You might receive the same webhook multiple times. Use payload.id and the event type to deduplicate.
async function handleWebhook(webhook) {
  const alreadyProcessed = await db.checkWebhookProcessed(
    webhook.payload.id,
    webhook.event
  );

  if (alreadyProcessed) {
    return; // Skip duplicate
  }

  await processValidation(webhook.payload);

  await db.markWebhookProcessed(
    webhook.payload.id,
    webhook.event
  );
}
Always return a 200 status code as quickly as possible to acknowledge receipt. Process the webhook asynchronously if needed.
app.post('/webhooks/kyc', async (req, res) => {
  // Acknowledge immediately
  res.status(200).send('OK');

  // Process asynchronously
  processWebhook(req.body).catch(console.error);
});
Always verify the X-Webhook-Signature header to ensure the webhook is authentic. See the security guide for details.
const signature = req.headers['x-webhook-signature'];
if (!verifySignature(req.rawBody, signature, secret)) {
  return res.status(401).json({ error: 'Invalid signature' });
}
If processing fails, log the error but still return 200 to prevent retries. Store failed webhooks for manual review.
try {
  await processWebhook(payload);
} catch (error) {
  await db.saveFailedWebhook({
    payload,
    error: error.message,
    receivedAt: new Date()
  });

  // Still return 200
  res.status(200).json({ success: false });
}

Troubleshooting

Check these items:
  • Webhook URL is publicly accessible via HTTPS
  • Webhook is configured and enabled in dashboard
  • Subscribed to correct KYC event types
  • Endpoint returns 200 status code within 30 seconds
  • Check server logs for incoming requests
extractedData and verifiedFields are only included in:
  • kyc.validation_approved
  • kyc.validation_rejected
They are not present in other event types like validation_created or validation_in_progress.
Common causes:
  • Using wrong secret (check dashboard for current secret)
  • Verifying signature on parsed JSON instead of raw body
  • Secret not properly saved after webhook creation
  • Encoding issues (ensure UTF-8)
See the security guide for proper implementation.
This is normal behavior. Webhooks may be sent multiple times due to network issues, timeouts, or retries.Always implement idempotency using the webhook payload.id (validation UUID) and event type.

Next Steps

Entity Events

Handle entity lifecycle events

Rule Events

Process compliance rule triggers

Webhook Security

Secure your webhook endpoints

Configuration

Configure webhook settings