Skip to main content

Overview

Webhooks allow you to receive real-time notifications when a KYC verification status changes. gu1 will automatically send HTTP POST requests to your configured webhook endpoint whenever a validation status updates.

Why Use 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

Webhook 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 begins the verification process
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)

Setting Up Webhooks

Step 1: Configure Webhook URL

Configure your webhook URL in the gu1 dashboard:
  1. Go to Settings β†’ Webhooks
  2. Add a new webhook endpoint (e.g., https://yourapp.com/webhooks/kyc)
  3. Subscribe to KYC events (kyc.*)
  4. Save and activate the webhook
Alternatively, you can specify a custom webhookUrl when creating individual validations.

Step 2: Create a Webhook Endpoint

Create an endpoint in your application to receive webhook POST requests:
app.post('/webhooks/kyc', async (req, res) => {
  try {
    const { type, data } = req.body;

    console.log('Received KYC webhook:', {
      type,
      validationId: data.validationId,
      status: data.status
    });

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

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

async function handleKycWebhook(type, 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 (type) {
    case 'kyc.validation_created':
      console.log('KYC validation created for:', entity.name);
      break;

    case 'kyc.validation_in_progress':
      await notifyCustomer(entity.externalId, 'verification-started');
      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
      });

      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
      });

      await notifyCustomer(entity.externalId, 'verification-rejected');
      break;

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

    case 'kyc.validation_expired':
      await notifyCustomer(entity.externalId, 'verification-expired');
      break;
  }
}

Step 3: Make Your Endpoint Publicly Accessible

Your webhook endpoint must be:
  • Publicly accessible via HTTPS
  • Able to receive POST requests
  • Return a 200 status code to acknowledge receipt
For local development, use tools like ngrok to create a public URL that tunnels to your local server.

Webhook Payload Structure

All webhooks follow this standard structure:
{
  "id": "evt_abc123",
  "type": "kyc.validation_approved",
  "created": "2025-01-15T11:00:00Z",
  "data": {
    "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"
  }
}

Common Payload Fields

id
string
Unique identifier for this webhook event
type
string
The event type (e.g., kyc.validation_approved)
created
string
ISO 8601 timestamp when the event occurred
data
object
Event-specific data (see below for each event type)
data.validationId
string
The KYC validation ID in gu1
data.entityId
string
The entity (person) ID being verified
data.entity
object
Entity information including your externalId for easy lookup
data.status
string
Current validation status: pending, in_progress, approved, rejected, abandoned, expired

Event-Specific Payloads

kyc.validation_created

Sent when a new KYC validation is created.
{
  "type": "kyc.validation_created",
  "data": {
    "validationId": "550e8400-e29b-41d4-a716-446655440000",
    "entityId": "123e4567-e89b-12d3-a456-426614174000",
    "entity": { ... },
    "status": "pending"
  }
}

kyc.validation_in_progress

Sent when a customer starts the verification process.
{
  "type": "kyc.validation_in_progress",
  "data": {
    "validationId": "550e8400-e29b-41d4-a716-446655440000",
    "entityId": "123e4567-e89b-12d3-a456-426614174000",
    "entity": { ... },
    "status": "in_progress"
  }
}

kyc.validation_approved

Sent when verification is successfully completed.
{
  "type": "kyc.validation_approved",
  "data": {
    "validationId": "550e8400-e29b-41d4-a716-446655440000",
    "entityId": "123e4567-e89b-12d3-a456-426614174000",
    "entity": { ... },
    "status": "approved",
    "verifiedAt": "2025-01-15T11:00:00Z",
    "extractedData": {
      "firstName": "John",
      "lastName": "Doe",
      "dateOfBirth": "1990-05-20",
      "nationality": "US",
      "documentNumber": "AB123456",
      "documentType": "passport"
    },
    "verifiedFields": [
      "firstName",
      "lastName",
      "dateOfBirth",
      "nationality",
      "documentNumber"
    ],
    "warnings": [],
    "decision": {
      "id_verification": "pass",
      "face_match": "pass",
      "liveness": "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
  • decision: Verification results (images/URLs removed for security)

kyc.validation_rejected

Sent when verification fails.
{
  "type": "kyc.validation_rejected",
  "data": {
    "validationId": "550e8400-e29b-41d4-a716-446655440000",
    "entityId": "123e4567-e89b-12d3-a456-426614174000",
    "entity": { ... },
    "status": "rejected",
    "verifiedAt": "2025-01-15T11:00:00Z",
    "extractedData": {},
    "verifiedFields": [],
    "warnings": [
      "Document authenticity check failed",
      "Face match confidence low"
    ],
    "decision": {
      "id_verification": "fail",
      "face_match": "fail",
      "liveness": "pass"
    }
  }
}

kyc.validation_abandoned

Sent when a customer starts but doesn’t complete the verification.
{
  "type": "kyc.validation_abandoned",
  "data": {
    "validationId": "550e8400-e29b-41d4-a716-446655440000",
    "entityId": "123e4567-e89b-12d3-a456-426614174000",
    "entity": { ... },
    "status": "abandoned"
  }
}

kyc.validation_expired

Sent when a validation session expires without completion.
{
  "type": "kyc.validation_expired",
  "data": {
    "validationId": "550e8400-e29b-41d4-a716-446655440000",
    "entityId": "123e4567-e89b-12d3-a456-426614174000",
    "entity": { ... },
    "status": "expired",
    "expiresAt": "2025-01-22T10:30:00Z"
  }
}

Best Practices

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);
});
You might receive the same webhook multiple times. Use the id field to ensure you process each event only once.
async function handleWebhook(webhook) {
  const alreadyProcessed = await db.checkWebhookProcessed(webhook.id);

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

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

  // Mark as processed
  await db.markWebhookProcessed(webhook.id);
}
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
});
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 });
}
Future versions will include webhook signatures. For now, ensure your webhook URL is kept private and uses HTTPS.

Testing Webhooks

Local Development

Use ngrok to expose your local server:
# Start ngrok
ngrok http 3000

# Use the ngrok URL as your webhook URL
https://abc123.ngrok.io/webhooks/kyc

Testing Flow

  1. Create a test KYC validation
  2. Your webhook endpoint receives kyc.validation_created
  3. Customer completes verification
  4. Your webhook endpoint receives kyc.validation_approved or kyc.validation_rejected

Troubleshooting

Check these items:
  • Webhook URL is publicly accessible via HTTPS
  • Firewall allows incoming POST requests
  • Endpoint returns 200 status code
  • Webhook is configured and active in gu1 dashboard
  • Check server logs for incoming requests
This is normal. Implement idempotency checks using the webhook id field to handle duplicates.
Process webhooks asynchronously. Return 200 immediately and handle business logic in background jobs.
extractedData and verifiedFields are only included in kyc.validation_approved and kyc.validation_rejected events.

Next Steps