Skip to main content

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

All KYC webhook events follow this standard structure:
{
  "event": "kyc.validation_approved",
  "timestamp": "2025-01-07T11:00:00Z",
  "organizationId": "org-123",
  "payload": {
    "validationId": "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"
    },
    "status": "approved"
    // ... event-specific fields
  }
}

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.validationId
string
The KYC validation ID in Gu1
payload.entityId
string
The entity (person) ID being verified
payload.entity
object
Entity information including your externalId for easy lookup
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.
{
  "event": "kyc.validation_created",
  "timestamp": "2025-01-07T10:30:00Z",
  "organizationId": "org-123",
  "payload": {
    "validationId": "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"
    },
    "status": "pending",
    "validationUrl": "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.
{
  "event": "kyc.validation_in_progress",
  "timestamp": "2025-01-07T10:35:00Z",
  "organizationId": "org-123",
  "payload": {
    "validationId": "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"
    },
    "status": "in_progress",
    "startedAt": "2025-01-07T10:35:00Z"
  }
}
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.
{
  "event": "kyc.validation_in_review",
  "timestamp": "2025-01-07T10:50:00Z",
  "organizationId": "org-123",
  "payload": {
    "validationId": "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"
    },
    "status": "in_review",
    "completedAt": "2025-01-07T10:50:00Z"
  }
}
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.
{
  "event": "kyc.validation_approved",
  "timestamp": "2025-01-07T11:00:00Z",
  "organizationId": "org-123",
  "payload": {
    "validationId": "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"
    },
    "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:
  • 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.
{
  "event": "kyc.validation_rejected",
  "timestamp": "2025-01-07T11:00:00Z",
  "organizationId": "org-123",
  "payload": {
    "validationId": "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"
    },
    "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:
  • 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.
{
  "event": "kyc.validation_abandoned",
  "timestamp": "2025-01-07T10:45:00Z",
  "organizationId": "org-123",
  "payload": {
    "validationId": "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"
    },
    "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.
{
  "event": "kyc.validation_expired",
  "timestamp": "2025-01-14T10:30:00Z",
  "organizationId": "org-123",
  "payload": {
    "validationId": "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"
    },
    "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.
{
  "event": "kyc.validation_cancelled",
  "timestamp": "2025-01-07T12:00:00Z",
  "organizationId": "org-123",
  "payload": {
    "validationId": "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"
    },
    "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.validationId,
      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, entityId, entity, status } = data;

  // Update your database with validation ID from Gu1
  await db.updateEntity(entity.externalId, {
    kycValidationId: validationId,
    kycStatus: status,
    lastUpdated: new Date()
  });

  // Perform actions based on event type
  switch (event) {
    case 'kyc.validation_created':
      console.log('KYC validation created for:', entity.name);
      // Send validation URL to customer
      await sendValidationEmail(entity, data.validationUrl);
      break;

    case 'kyc.validation_in_progress':
      await notifyCustomer(entity.externalId, 'verification-started');
      break;

    case 'kyc.validation_in_review':
      // Notify compliance team
      await notifyComplianceTeam(entity, validationId);
      await notifyCustomer(entity.externalId, 'verification-in-review');
      break;

    case 'kyc.validation_approved':
      // Extract verified data
      const { extractedData, verifiedFields } = data;

      await db.updateEntity(entity.externalId, {
        verifiedData: extractedData,
        verifiedFields: verifiedFields,
        verifiedAt: data.verifiedAt,
        isVerified: true
      });

      // Activate customer account
      await activateCustomerAccount(entity.externalId);
      await notifyCustomer(entity.externalId, 'verification-approved');
      break;

    case 'kyc.validation_rejected':
      await db.updateEntity(entity.externalId, {
        isVerified: false,
        rejectionReasons: data.warnings,
        rejectionReason: data.rejectionReason
      });

      await notifyCustomer(entity.externalId, 'verification-rejected', {
        reasons: data.warnings
      });
      break;

    case 'kyc.validation_abandoned':
      await notifyCustomer(entity.externalId, 'verification-incomplete', {
        lastStep: data.lastStep
      });
      break;

    case 'kyc.validation_expired':
      await notifyCustomer(entity.externalId, 'verification-expired');
      // Clean up expired validation
      await db.deleteValidation(validationId);
      break;

    case 'kyc.validation_cancelled':
      await notifyCustomer(entity.externalId, 'verification-cancelled');
      // Clean up associated resources
      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['validationId']
    entity_id = data['entityId']
    entity = data['entity']
    status = data['status']

    # Update database
    db.update_entity(
        external_id=entity['externalId'],
        kyc_validation_id=validation_id,
        kyc_status=status,
        last_updated=datetime.now()
    )

    # Handle different events
    if event == 'kyc.validation_created':
        send_validation_email(entity, data['validationUrl'])

    elif event == 'kyc.validation_in_review':
        notify_compliance_team(entity, validation_id)
        notify_customer(entity['externalId'], 'verification-in-review')

    elif event == 'kyc.validation_approved':
        db.update_entity(
            external_id=entity['externalId'],
            verified_data=data.get('extractedData'),
            verified_fields=data.get('verifiedFields'),
            verified_at=data.get('verifiedAt'),
            is_verified=True
        )
        activate_customer_account(entity['externalId'])
        notify_customer(entity['externalId'], 'verification-approved')

    elif event == 'kyc.validation_rejected':
        db.update_entity(
            external_id=entity['externalId'],
            is_verified=False,
            rejection_reasons=data.get('warnings')
        )
        notify_customer(entity['externalId'], 'verification-rejected')

    elif event == 'kyc.validation_abandoned':
        notify_customer(entity['externalId'], 'verification-incomplete')

    elif event == 'kyc.validation_expired':
        notify_customer(entity['externalId'], 'verification-expired')
        db.delete_validation(validation_id)

    elif event == 'kyc.validation_cancelled':
        notify_customer(entity['externalId'], '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 { entity, validationId, lastStep } = data;

  // Schedule reminder email
  await queue.scheduleEmail({
    to: entity.attributes?.email,
    subject: 'Complete your verification',
    template: 'kyc-reminder',
    data: {
      name: entity.name,
      lastStep,
      validationUrl: `https://kyc.gu1.io/validate/${validationId}`
    },
    sendAt: new Date(Date.now() + 24 * 60 * 60 * 1000) // Send in 24 hours
  });

  // Track abandonment
  await analytics.track({
    event: 'kyc_abandoned',
    entityId: entity.id,
    lastStep,
    timestamp: new Date()
  });
}

Best Practices

The webhook includes entity.externalId which is the ID you provided when creating the entity. Use this to look up the customer in your database.
const customer = await db.findCustomer({
  externalId: data.entity.externalId
});
Store the validationId from Gu1 in your database. This allows you to query validation details later if needed.
await db.updateCustomer(customer.id, {
  kycValidationId: data.validationId,
  kycStatus: data.status
});
You might receive the same webhook multiple times. Use the validationId to ensure you process each event only once.
async function handleWebhook(webhook) {
  const alreadyProcessed = await db.checkWebhookProcessed(
    webhook.payload.validationId,
    webhook.event
  );

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

  // Process webhook
  await processValidation(webhook.payload);

  // Mark as processed
  await db.markWebhookProcessed(
    webhook.payload.validationId,
    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 validationId and event type.

Next Steps