Skip to main content

Overview

Webhooks allow your application to receive real-time notifications when users complete authentication, signatures, or message actions. UIP sends POST requests to your configured webhook URL with cryptographically signed payloads.
Required Setup: You must configure your webhook URL in the Business Dashboard settings before using any UIP API endpoints. All API requests will fail with request/webhook-missing error if no webhook URL is configured.

Setting Up Webhooks

1. Configure Webhook URL

  1. Log into the Business Dashboard
  2. Navigate to Settings > Webhooks
  3. Enter your webhook endpoint URL (must use HTTPS in production)
  4. Save your configuration
HTTPS Required: Production webhook URLs must use HTTPS. HTTP URLs are not allowed for security reasons.

2. Retrieve Webhook Secret

Your webhook secret is used to verify that requests are actually from UIP and haven’t been tampered with.
  1. In the Business Dashboard, go to Settings > Webhooks
  2. Copy your Webhook Secret (shown after saving your webhook URL)
  3. Store this secret securely in your application’s environment variables
Keep Secret Secure: Never commit your webhook secret to version control or expose it in client-side code. Treat it like a password.

Verifying Webhook Signatures

Every webhook request includes an X-UIP-Signature header containing an HMAC SHA-256 signature. You must verify this signature to ensure requests are from UIP.

Signature Verification Algorithm

  1. Get the X-UIP-Signature header from the request
  2. Calculate HMAC SHA-256 hash of the raw request body using your webhook secret
  3. Compare the calculated signature with the header value
  4. Reject the request if signatures don’t match

Implementation Examples

const crypto = require('crypto');

app.post('/webhook', (req, res) => {
  const signature = req.headers['x-uip-signature'];
  const webhookSecret = process.env.UIP_WEBHOOK_SECRET;

  // Calculate expected signature
  const hmac = crypto.createHmac('sha256', webhookSecret);
  hmac.update(JSON.stringify(req.body));
  const expectedSignature = hmac.digest('hex');

  // Verify signature
  if (signature !== expectedSignature) {
    return res.status(401).send('Invalid signature');
  }

  // Handle webhook event
  const { event, data } = req.body;

  res.status(200).send('OK');
});

Webhook Events

UIP sends different event types depending on which API was used. All webhooks follow the same structure:
{
  "event": "event_type",
  "data": {
    // Event-specific data
  }
}

Event: identify

Sent when a user completes authentication via the Identify API.
{
  "event": "identify",
  "data": {
    "uip_id": "user_abc123def456",
    "audit_id": "audit_9z8y7x6w5v4u",
    "session_id": "sess_1a2b3c4d5e6f",
    "name": "John Doe",
    "date_of_birth": "1990-05-15",
    "country_of_origin": "US"
  }
}
Fields:
  • uip_id - User’s unique UIP identifier
  • audit_id - Permanent audit trail reference (save for compliance)
  • session_id - Session ID from your original request
  • name - User’s full name (if requested)
  • date_of_birth - User’s date of birth in YYYY-MM-DD format (if requested)
  • country_of_origin - User’s country code (if requested)

Event: sign

Sent when a user signs via the Sign API.
{
  "event": "sign",
  "data": {
    "uip_id": "user_abc123def456",
    "session_id": "sess_1a2b3c4d5e6f",
    "audit_id": "audit_9z8y7x6w5v4u"
  }
}
Fields:
  • uip_id - User’s unique UIP identifier
  • session_id - Session ID from your original request
  • audit_id - Permanent audit trail reference (save for compliance)

Event: message

Sent when a user signs or declines a signature-required message via the Message API.
{
  "event": "message",
  "data": {
    "signing_uip_id": "user_abc123def456",
    "message_id": "msg_xyz789abc123",
    "audit_id": "audit_9z8y7x6w5v4u"
  }
}
Fields:
  • signing_uip_id - UIP ID of the user who signed or declined
  • message_id - Message ID from your original request
  • audit_id - Permanent audit trail reference (save for compliance)
Determining Action: The webhook structure is identical for both signed and declined messages. Query the Audit API using the audit_id to determine if the user signed or declined.

Event: test

Sent when you test your webhook configuration via the Test Webhook endpoint.
{
  "event": "test",
  "data": {
    "pong": "Pong"
  }
}
Purpose: Verify your webhook endpoint is reachable and correctly validates signatures.

When Webhooks Are Sent

Webhook sent when: User completes biometric authenticationNo webhook sent when: User cancels or session expires (5 minutes)
Webhook sent when: User signs with biometric verificationNo webhook sent when: User declines or session expires (5 minutes)
Webhook sent when: User signs or declines a signature-required message (signature_required: true)No webhook sent when:
  • Message does not require signature (signature_required: false)
  • User doesn’t respond and message expires
  • Message is invalidated before user responds
Webhook sent when: You call the Test Webhook endpointPurpose: Verify your webhook configuration before production use

Best Practices

Always Verify Signatures

Validate the X-UIP-Signature header on every request to prevent spoofing and tampering

Return 200 Quickly

Process webhooks asynchronously and return 200 immediately to avoid timeouts

Store Audit IDs

Save audit_id from every webhook for permanent proof, compliance, and legal verification

Handle Idempotency

UIP may send the same webhook multiple times. Use audit_id or session_id to detect duplicates

Use HTTPS

Production webhook URLs must use HTTPS. HTTP is not allowed for security reasons.

Test Before Production

Use the Test Webhook endpoint during development to verify your setup works correctly

Error Handling

If your webhook endpoint returns an error (non-200 status code) or times out, UIP will retry the webhook with exponential backoff. Retry Schedule:
  • Immediate retry
  • Retry after 5 seconds
  • Retry after 30 seconds
  • Retry after 2 minutes
  • Retry after 10 minutes
After all retries fail, the webhook is marked as failed and will not be retried again.
Handle Retries: Design your webhook handler to be idempotent. Use audit_id or session_id to detect duplicate webhook deliveries.

Webhook Handler Template

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

const app = express();
app.use(express.json());

app.post('/webhook', async (req, res) => {
  // 1. Verify signature
  const signature = req.headers['x-uip-signature'];
  const webhookSecret = process.env.UIP_WEBHOOK_SECRET;

  const hmac = crypto.createHmac('sha256', webhookSecret);
  hmac.update(JSON.stringify(req.body));
  const expectedSignature = hmac.digest('hex');

  if (signature !== expectedSignature) {
    return res.status(401).send('Invalid signature');
  }

  // 2. Return 200 immediately
  res.status(200).send('OK');

  // 3. Process webhook asynchronously
  const { event, data } = req.body;

  try {
    switch (event) {
      case 'identify':
        await handleIdentify(data);
        break;
      case 'sign':
        await handleSign(data);
        break;
      case 'message':
        await handleMessage(data);
        break;
      case 'test':
        console.log('Test webhook received');
        break;
      default:
        console.warn(`Unknown event type: ${event}`);
    }
  } catch (error) {
    console.error('Webhook processing error:', error);
  }
});

async function handleIdentify(data) {
  const { uip_id, audit_id, session_id, name, date_of_birth, country_of_origin } = data;

  // Check for duplicate
  const existing = await db.identifications.findOne({ audit_id });
  if (existing) {
    console.log('Duplicate webhook, skipping');
    return;
  }

  // Save to database
  await db.identifications.create({
    uip_id,
    audit_id,
    session_id,
    name,
    date_of_birth,
    country_of_origin,
    received_at: new Date()
  });

  // Complete user's action
  await completeUserLogin(session_id);
}

async function handleSign(data) {
  const { uip_id, session_id, audit_id } = data;

  // Check for duplicate
  const existing = await db.signatures.findOne({ audit_id });
  if (existing) {
    console.log('Duplicate webhook, skipping');
    return;
  }

  // Save to database
  await db.signatures.create({
    uip_id,
    audit_id,
    session_id,
    received_at: new Date()
  });

  // Complete signature flow
  await completeSignature(session_id);
}

async function handleMessage(data) {
  const { signing_uip_id, message_id, audit_id } = data;

  // Check for duplicate
  const existing = await db.messageSignatures.findOne({ audit_id });
  if (existing) {
    console.log('Duplicate webhook, skipping');
    return;
  }

  // Query audit API to check if signed or declined
  const auditResponse = await fetch(
    `https://api.uip.digital/v1/audit/${audit_id}`,
    {
      headers: { 'Authorization': `Bearer ${process.env.UIP_API_KEY}` }
    }
  );

  const { audit } = await auditResponse.json();
  const wasSigned = audit.signed_accepted;

  // Save to database
  await db.messageSignatures.create({
    signing_uip_id,
    message_id,
    audit_id,
    was_signed: wasSigned,
    received_at: new Date()
  });

  // Handle based on outcome
  if (wasSigned) {
    await handleMessageSigned(message_id);
  } else {
    await handleMessageDeclined(message_id);
  }
}

app.listen(3000, () => {
  console.log('Webhook server listening on port 3000');
});

Troubleshooting

Problem: API requests fail with request/webhook-unreachable errorCauses:
  • Webhook URL is not publicly accessible
  • Firewall blocking UIP servers
  • Webhook handler not returning 200 status code
  • HTTPS certificate issues
Solutions:
  • Verify webhook URL is publicly accessible
  • Check firewall rules allow incoming requests
  • Ensure webhook returns 200 status for all events
  • Use valid HTTPS certificate
Problem: Webhook receives requests but signature validation failsCauses:
  • Using wrong webhook secret
  • Calculating signature on modified body
  • Character encoding issues
Solutions:
  • Get webhook secret from Business Dashboard settings
  • Calculate signature on raw request body (before parsing)
  • Use UTF-8 encoding for all strings
Problem: API requests fail with request/webhook-missing errorCauses:
  • No webhook URL configured in Business Dashboard
  • Webhook URL field is empty
Solutions:
  • Log into Business Dashboard
  • Navigate to Settings > Webhooks
  • Add your webhook endpoint URL
Problem: Receiving the same webhook multiple timesCauses:
  • UIP retries webhooks if your server returns errors or times out
  • Network issues causing automatic retries
Solutions:
  • Use audit_id or session_id to detect duplicates
  • Make webhook handler idempotent (safe to process multiple times)
  • Store processed webhook IDs in database to skip duplicates