Skip to main content

Overview

Webhooks allow your application to receive real-time notifications for specific UIP events. UIP sends POST requests to your configured webhook URL with cryptographically signed payloads.
Webhooks are only used for:
  • Message API — when a user signs or declines a signature-required message
  • Authorize API — when a business owner approves delegation (authorize.completed)
  • Delegation revocation — when a business revokes platform access (delegation.revoked)
  • Webhook testing — when you call the test endpoint
Identify and Sign APIs do NOT use webhooks. Use polling with GET /v1/identify/:id or GET /v1/sign/:id instead.

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

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: authorize.completed

Sent when a business owner approves a platform delegation request via the Authorize API.
{
  "event": "authorize.completed",
  "session_id": "sess_auth_1a2b3c4d",
  "timestamp": "2025-01-11T12:31:00Z",
  "data": {
    "access_token": "uip_at_abc123xyz789..."
  }
}

Event: delegation.revoked

Sent when a business revokes a platform’s delegation access.
{
  "event": "delegation.revoked",
  "timestamp": "2025-01-11T12:31:00Z",
  "data": {
    "business_uip_id": "business_abc123"
  }
}

Event: test

Sent when you test your webhook configuration via the Test Webhook endpoint.
{
  "event": "test",
  "timestamp": "2025-01-11T12:30:00Z",
  "test": true
}
Purpose: Verify your webhook endpoint is reachable and correctly validates signatures.

When Webhooks Are Sent

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: Business owner approves a delegation requestNo webhook sent when: Authorization session expires without approval
Webhook sent when: A business revokes your platform’s delegation access
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 message_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: UIP retries webhook delivery once after a short delay. If both attempts fail, the webhook is marked as failed.
  • Attempt 1: Immediate
  • Attempt 2: After a short delay
Handle Retries: Design your webhook handler to be idempotent. Use audit_id or message_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 'message':
        await handleMessage(data);
        break;
      case 'authorize.completed':
        await handleAuthorizeCompleted(data);
        break;
      case 'delegation.revoked':
        await handleDelegationRevoked(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 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);
  }
}

async function handleAuthorizeCompleted(data) {
  const { access_token } = data;
  // Store delegation token securely
  await db.delegations.create({
    access_token,
    received_at: new Date()
  });
}

async function handleDelegationRevoked(data) {
  const { business_uip_id } = data;
  // Remove stored delegation token
  await db.delegations.delete({ business_uip_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 message_id to detect duplicates
  • Make webhook handler idempotent (safe to process multiple times)
  • Store processed webhook IDs in database to skip duplicates