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 Type Description When Triggered kyc.validation_createdValidation session created When you create a new KYC validation kyc.validation_in_progressCustomer started verification Customer is actively completing verification (filling out form) kyc.validation_in_reviewVerification under review Verification completed, requires manual review from compliance team kyc.validation_approvedVerification approved Identity successfully verified kyc.validation_rejectedVerification rejected Identity verification failed kyc.validation_abandonedCustomer abandoned process Customer left without completing kyc.validation_expiredValidation session expired Session expired (typically after 7 days) kyc.validation_cancelledValidation cancelled Validation 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
The event type (e.g., kyc.validation_approved)
ISO 8601 timestamp when the event occurred
The KYC validation ID in Gu1 (primary key of the validation row)
The entity (person) ID being verified
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).
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
Use entity.externalId for Lookup (terminal events)
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
Signature Verification Failing
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.
Receiving Duplicate Webhooks
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