Webhook Notifications

Overview

Webhooks allow you to receive real-time notifications when events occur in your Unter account, such as successful payments or payment failures. Instead of constantly polling our API for updates, webhooks push notifications directly to your server when something important happens.

How Webhooks Work

  1. Configure your webhook URL - Tell Unter where to send notifications

  2. Event occurs - A payment succeeds, fails, etc.

  3. Unter sends HTTP POST - We POST event data to your URL

  4. Your server responds - Return 200 OK to acknowledge receipt

  5. Retry if needed - We retry failed deliveries with exponential backoff

Webhook Events

payment.succeeded

Fired when a payment is successfully confirmed on the blockchain.

Event Type: payment.succeeded

Payload:

{
  "id": "evt_1a2b3c4d5e6f7g8h",
  "type": "payment.succeeded",
  "created": 1690876543,
  "data": {
    "object": {
      "id": "pay_9z8y7x6w5v4u3t2s",
      "amount": {
        "raw": "5000000",
        "formatted": "5.00",
        "decimals": 6
      },
      "currency": "USDC",
      "status": "succeeded",
      "reference": "order_12345",
      "source_chain": "Ethereum",
      "destination_chain": "Polygon",
      "source_tx_hash": "0xabcd1234...",
      "destination_tx_hash": "0xefgh5678...",
      "payer_address": "0x742C4B8C2515Ad1D8e1C71e8e24c8D5F9A1a8B5A",
      "created_at": "2024-08-14T13:45:00+00:00",
      "completed_at": "2024-08-14T13:47:00+00:00"
    }
  }
}

payment.failed

Fired when a payment fails to process or confirm.

Event Type: payment.failed

Payload:

{
  "id": "evt_2b3c4d5e6f7g8h9i",
  "type": "payment.failed",
  "created": 1690876600,
  "data": {
    "object": {
      "id": "pay_8y7x6w5v4u3t2s1r",
      "amount": {
        "raw": "2500000",
        "formatted": "2.50",
        "decimals": 6
      },
      "currency": "USDC",
      "status": "failed",
      "reference": "order_67890",
      "source_chain": "Ethereum",
      "destination_chain": "Polygon",
      "source_tx_hash": "0x1234abcd...",
      "destination_tx_hash": null,
      "payer_address": "0x853D5C9F3A2E7B4F8C1E6A9D2B5F8E3C7A6B9E2A",
      "created_at": "2024-08-14T14:15:00+00:00",
      "completed_at": null
    }
  }
}

Webhook Security

Unter signs all webhook payloads using HMAC-SHA256 with your webhook secret. This ensures the webhook is actually from Unter and hasn't been tampered with.

Getting Your Webhook Secret: You can find your webhook secret in the Unter merchant portal under the webhook settings section. The secret is automatically generated when you first configure your webhook URL.

Signature Verification

Each webhook includes a Unter-Signature header with the following format:

Unter-Signature: t=1690876543,v1=5d41402abc4b2a76b9719d911017c592

Where:

  • t = Unix timestamp when the signature was generated

  • v1 = HMAC-SHA256 hex digest of the signed payload

Verifying Signatures

Step 1: Extract timestamp and signature

t=1690876543,v1=5d41402abc4b2a76b9719d911017c592

Step 2: Create signed payload Concatenate the timestamp, a period, and the raw request body:

1690876543.{"id":"evt_123","type":"payment.succeeded",...}

Step 3: Generate expected signature

$expectedSignature = hash_hmac('sha256', $signedPayload, $webhookSecret);

Step 4: Compare signatures Use constant-time comparison to prevent timing attacks:

if (hash_equals($expectedSignature, $receivedSignature)) {
    // Signature is valid
}

Step 5: Check timestamp tolerance Reject webhooks older than 5 minutes (300 seconds) to prevent replay attacks:

if (abs(time() - $timestamp) > 300) {
    // Webhook too old, reject
}

Retry Logic

Unter automatically retries failed webhook deliveries using exponential backoff:

  • Attempt 1: Immediately

  • Attempt 2: After 1 minute

  • Attempt 3: After 3 minutes

  • Attempt 4: After 5 minutes

  • Attempt 5: After 10 minutes

  • Attempt 6: After 30 minutes

  • Attempt 7: After 2 hours

A webhook is considered failed if:

  • Your server doesn't respond within 10 seconds

  • Your server returns a non-2xx HTTP status code

  • Network connection fails

After 7 failed attempts, we stop retrying and mark the webhook as permanently failed.

Implementation Examples

JavaScript (Node.js with Express)

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

const app = express();

// Middleware to capture raw body for signature verification
app.use('/webhooks', express.raw({ type: 'application/json' }));

const WEBHOOK_SECRET = process.env.UNTER_WEBHOOK_SECRET;

function verifyWebhookSignature(payload, signature, secret) {
  // Parse signature header
  const elements = {};
  signature.split(',').forEach(pair => {
    const [key, value] = pair.split('=');
    elements[key] = value;
  });

  if (!elements.t || !elements.v1) {
    return false;
  }

  const timestamp = parseInt(elements.t);
  
  // Check timestamp tolerance (5 minutes)
  if (Math.abs(Date.now() / 1000 - timestamp) > 300) {
    return false;
  }

  // Create signed payload
  const signedPayload = `${timestamp}.${payload}`;
  
  // Generate expected signature
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');

  // Compare signatures
  return crypto.timingSafeEqual(
    Buffer.from(expectedSignature, 'hex'),
    Buffer.from(elements.v1, 'hex')
  );
}

app.post('/webhooks/unter', (req, res) => {
  const signature = req.headers['unter-signature'];
  const payload = req.body.toString();

  // Verify signature
  if (!verifyWebhookSignature(payload, signature, WEBHOOK_SECRET)) {
    console.error('Invalid webhook signature');
    return res.status(400).send('Invalid signature');
  }

  // Parse webhook data
  const event = JSON.parse(payload);
  
  console.log('Received webhook:', event.type, event.id);

  // Handle the event
  switch (event.type) {
    case 'payment.succeeded':
      handlePaymentSucceeded(event.data.object);
      break;
    
    case 'payment.failed':
      handlePaymentFailed(event.data.object);
      break;
      
    default:
      console.log('Unhandled event type:', event.type);
  }

  // Always respond with 200 OK
  res.status(200).send('OK');
});

function handlePaymentSucceeded(payment) {
  console.log('Payment succeeded:', payment.id);
  console.log('Amount:', payment.amount.formatted, payment.currency);
  console.log('Reference:', payment.reference);
  
  // Update your database
  // Send confirmation email
  // Fulfill the order
}

function handlePaymentFailed(payment) {
  console.log('Payment failed:', payment.id);
  console.log('Reference:', payment.reference);
  
  // Log the failure
  // Notify customer
  // Update order status
}

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

Best Practices

1. Always Verify Signatures

Never process webhooks without verifying the signature. This prevents malicious actors from sending fake webhooks to your server.

2. Handle Idempotency

The same webhook may be delivered multiple times. Use the event ID to ensure you only process each event once:

const processedEvents = new Set();

app.post('/webhooks/unter', (req, res) => {
  const event = JSON.parse(req.body);
  
  // Check if we've already processed this event
  if (processedEvents.has(event.id)) {
    return res.status(200).send('Already processed');
  }
  
  // Process the event
  handleEvent(event);
  
  // Mark as processed
  processedEvents.add(event.id);
  
  res.status(200).send('OK');
});

3. Respond Quickly

Respond with 200 OK as quickly as possible. Do heavy processing (database updates, email sending) asynchronously:

4. Handle Failures Gracefully

Your webhook endpoint should be fault-tolerant and handle errors gracefully:

app.post('/webhooks/unter', (req, res) => {
  try {
    // Process webhook
    processWebhook(req.body);
    res.status(200).send('OK');
  } catch (error) {
    console.error('Webhook processing failed:', error);
    // Return 200 to prevent retries for unrecoverable    errors
    res.status(200).send('Error logged');
  }
});

Common Issues

1. Signature Verification Failures

Causes:

  • Wrong webhook secret

  • Modifying request body before verification

  • Clock synchronization issues

Solutions:

  • Verify you're using the correct secret from the webhook settings

  • Verify signature before parsing/modifying the request body

  • Ensure server time is synchronized with NTP

2. Webhook Timeouts

Causes:

  • Slow database queries in webhook handler

  • Synchronous external API calls

  • Heavy processing in webhook handler

Solutions:

  • Respond with 200 OK immediately

  • Process webhooks asynchronously

  • Optimize database queries

3. Duplicate Processing

Causes:

  • Not implementing idempotency

  • Multiple server instances processing same webhook

Solutions:

  • Use event ID to track processed webhooks

  • Implement proper database locking/transactions

Security Considerations

  1. Always verify signatures - Never trust unverified webhooks

  2. Validate timestamp - Reject old webhooks to prevent replay attacks

Last updated