Webhooks

Real-time event notifications and webhook integration guide for the Appmint platform

Appmint Developer Relations1/15/202418 min read

Webhooks

Webhooks provide real-time notifications about events happening in your Appmint application. Instead of polling our APIs, webhooks allow your application to receive instant updates when important events occur, enabling you to build reactive, real-time experiences.

What are Webhooks?

Event-Driven Architecture

Webhooks are HTTP callbacks that Appmint sends to your application when specific events occur. When an event happens (like a new user registration or a payment completion), Appmint immediately sends an HTTP POST request to your configured webhook endpoint with detailed information about the event.

Key Benefits:

  • Real-time Updates - Receive events as they happen
  • Reduced API Calls - No need for constant polling
  • Scalable Architecture - Handle high-volume events efficiently
  • Reliable Delivery - Built-in retry mechanism and failure handling
  • Secure Communication - Cryptographic signature verification

Webhook Events

Available Event Types

Authentication Events:

  • user.created - New user registration
  • user.updated - User profile changes
  • user.deleted - User account deletion
  • user.login - User login activity
  • user.logout - User logout activity
  • password.reset - Password reset request

Database Events:

  • document.created - New document in collection
  • document.updated - Document modification
  • document.deleted - Document deletion
  • collection.created - New collection created
  • collection.deleted - Collection removed

File Storage Events:

  • file.uploaded - New file upload
  • file.deleted - File deletion
  • file.processing.completed - File processing finished
  • file.processing.failed - File processing error

Payment Events:

  • payment.succeeded - Successful payment
  • payment.failed - Failed payment
  • payment.refunded - Payment refund
  • subscription.created - New subscription
  • subscription.updated - Subscription changes
  • subscription.cancelled - Subscription cancellation

Workflow Events:

  • workflow.started - Workflow execution started
  • workflow.completed - Workflow execution completed
  • workflow.failed - Workflow execution failed
  • task.created - New task created
  • task.completed - Task completion

System Events:

  • application.deployed - New deployment
  • application.error - Application error
  • api.rate_limit_exceeded - Rate limit reached
  • backup.completed - Backup operation finished

Setting Up Webhooks

Creating Webhook Endpoints

1. Dashboard Configuration

// Navigate to Project Settings > Webhooks in the Appmint dashboard
// Add a new webhook endpoint

const webhookConfig = {
  url: 'https://your-app.com/webhooks/appmint',
  events: [
    'user.created',
    'payment.succeeded', 
    'document.created'
  ],
  active: true,
  secret: 'your-webhook-secret', // For signature verification
  headers: {
    'Authorization': 'Bearer your-api-token',
    'X-Custom-Header': 'value'
  }
};

2. Programmatic Setup

// Create webhook via API
const webhook = await appmint.webhooks.create({
  url: 'https://your-app.com/api/webhooks/appmint',
  events: ['user.created', 'payment.succeeded'],
  description: 'Main application webhook',
  metadata: {
    environment: 'production',
    version: '1.0'
  }
});

console.log('Webhook created:', webhook.id);

Webhook Endpoint Implementation

Node.js/Express Example

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

// Webhook endpoint
app.post('/webhooks/appmint', (req, res) => {
  const signature = req.headers['x-appmint-signature'];
  const payload = req.body;
  
  // Verify webhook signature
  if (!verifySignature(payload, signature, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send('Unauthorized');
  }
  
  // Parse the event
  const event = JSON.parse(payload);
  
  // Handle the event
  handleWebhookEvent(event);
  
  // Respond with 200 to acknowledge receipt
  res.status(200).send('OK');
});

function verifySignature(payload, signature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
    
  return crypto.timingSafeEqual(
    Buffer.from(signature.split('=')[1], 'hex'),
    Buffer.from(expectedSignature, 'hex')
  );
}

async function handleWebhookEvent(event) {
  console.log('Received webhook:', event.type);
  
  switch (event.type) {
    case 'user.created':
      await handleNewUser(event.data);
      break;
      
    case 'payment.succeeded':
      await handleSuccessfulPayment(event.data);
      break;
      
    case 'document.created':
      await handleNewDocument(event.data);
      break;
      
    default:
      console.log('Unhandled event type:', event.type);
  }
}

async function handleNewUser(userData) {
  // Send welcome email
  await emailService.sendWelcomeEmail(userData.email, userData.name);
  
  // Create user in external CRM
  await crmService.createContact(userData);
  
  // Track analytics event
  await analytics.track('user_registered', {
    userId: userData.id,
    email: userData.email,
    source: userData.source
  });
}

Python/Flask Example

import hashlib
import hmac
import json
from flask import Flask, request, abort

app = Flask(__name__)

@app.route('/webhooks/appmint', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-Appmint-Signature')
    payload = request.get_data()
    
    # Verify signature
    if not verify_signature(payload, signature, app.config['WEBHOOK_SECRET']):
        abort(401)
    
    # Parse event
    event = json.loads(payload)
    
    # Handle event
    handle_webhook_event(event)
    
    return 'OK', 200

def verify_signature(payload, signature, secret):
    expected_signature = hmac.new(
        secret.encode('utf-8'),
        payload,
        hashlib.sha256
    ).hexdigest()
    
    provided_signature = signature.split('=')[1]
    
    return hmac.compare_digest(expected_signature, provided_signature)

def handle_webhook_event(event):
    event_type = event['type']
    data = event['data']
    
    handlers = {
        'user.created': handle_new_user,
        'payment.succeeded': handle_successful_payment,
        'document.created': handle_new_document
    }
    
    handler = handlers.get(event_type)
    if handler:
        handler(data)
    else:
        print(f'Unhandled event type: {event_type}')

def handle_new_user(user_data):
    # Send welcome email
    email_service.send_welcome_email(
        user_data['email'], 
        user_data['name']
    )
    
    # Add to mailing list
    mailchimp_service.add_subscriber(user_data['email'])

Java/Spring Boot Example

@RestController
@RequestMapping("/webhooks")
public class WebhookController {
    
    @Value("${webhook.secret}")
    private String webhookSecret;
    
    @PostMapping("/appmint")
    public ResponseEntity<String> handleWebhook(
            @RequestHeader("X-Appmint-Signature") String signature,
            @RequestBody String payload) {
        
        // Verify signature
        if (!verifySignature(payload, signature, webhookSecret)) {
            return ResponseEntity.status(401).body("Unauthorized");
        }
        
        // Parse event
        ObjectMapper mapper = new ObjectMapper();
        WebhookEvent event = mapper.readValue(payload, WebhookEvent.class);
        
        // Handle event
        handleWebhookEvent(event);
        
        return ResponseEntity.ok("OK");
    }
    
    private boolean verifySignature(String payload, String signature, String secret) {
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(), "HmacSHA256");
            mac.init(secretKey);
            
            byte[] expectedSignature = mac.doFinal(payload.getBytes());
            String expectedHex = Hex.encodeHexString(expectedSignature);
            String providedHex = signature.split("=")[1];
            
            return MessageDigest.isEqual(
                expectedHex.getBytes(),
                providedHex.getBytes()
            );
        } catch (Exception e) {
            return false;
        }
    }
    
    private void handleWebhookEvent(WebhookEvent event) {
        switch (event.getType()) {
            case "user.created":
                handleNewUser(event.getData());
                break;
            case "payment.succeeded":
                handleSuccessfulPayment(event.getData());
                break;
            default:
                log.info("Unhandled event type: {}", event.getType());
        }
    }
}

Webhook Event Structure

Standard Event Format

{
  "id": "evt_1234567890abcdef",
  "type": "user.created",
  "created": "2024-01-15T10:30:00Z",
  "api_version": "2024-01-01",
  "data": {
    "id": "user_abc123",
    "email": "user@example.com",
    "name": "John Doe",
    "created_at": "2024-01-15T10:30:00Z",
    "metadata": {
      "source": "signup_form",
      "utm_campaign": "winter_2024"
    }
  },
  "previous_attributes": {},
  "request": {
    "id": "req_xyz789",
    "idempotency_key": "key_unique_123"
  },
  "livemode": true
}

Event Field Descriptions

Top-level Fields:

  • id - Unique event identifier
  • type - Event type (e.g., 'user.created')
  • created - ISO 8601 timestamp when event was created
  • api_version - API version when event was generated
  • data - Event-specific data payload
  • previous_attributes - Previous values for update events
  • request - Information about the API request that triggered the event
  • livemode - Boolean indicating if this is a live or test event

Event-Specific Data

User Events

{
  "type": "user.created",
  "data": {
    "id": "user_abc123",
    "email": "john@example.com",
    "name": "John Doe",
    "email_verified": false,
    "phone": "+1234567890",
    "phone_verified": false,
    "profile": {
      "first_name": "John",
      "last_name": "Doe",
      "avatar_url": "https://cdn.appmint.io/avatars/abc123.jpg",
      "timezone": "America/New_York"
    },
    "metadata": {
      "signup_source": "mobile_app",
      "referrer": "friend_invite"
    },
    "created_at": "2024-01-15T10:30:00Z",
    "updated_at": "2024-01-15T10:30:00Z"
  }
}

Payment Events

{
  "type": "payment.succeeded",
  "data": {
    "id": "pay_def456",
    "amount": 2999,
    "currency": "usd",
    "customer_id": "user_abc123",
    "status": "succeeded",
    "payment_method": {
      "type": "card",
      "card": {
        "brand": "visa",
        "last4": "4242",
        "exp_month": 12,
        "exp_year": 2025
      }
    },
    "receipt_url": "https://receipts.appmint.io/pay_def456",
    "created_at": "2024-01-15T10:30:00Z"
  }
}

Document Events

{
  "type": "document.created",
  "data": {
    "id": "doc_ghi789",
    "collection": "posts",
    "document": {
      "title": "My First Post",
      "content": "Hello, world!",
      "author_id": "user_abc123",
      "published": false,
      "tags": ["introduction", "blog"],
      "created_at": "2024-01-15T10:30:00Z"
    },
    "metadata": {
      "source": "api",
      "ip_address": "192.168.1.1",
      "user_agent": "MyApp/1.0"
    }
  }
}

Security & Verification

Signature Verification

Why Verify Signatures?

Webhook signatures ensure that the HTTP requests you receive are actually from Appmint and haven't been tampered with during transmission.

Signature Algorithm

// Appmint uses HMAC SHA-256 for signatures
const crypto = require('crypto');

function generateSignature(payload, secret) {
  return crypto
    .createHmac('sha256', secret)
    .update(payload, 'utf8')
    .digest('hex');
}

function verifyWebhookSignature(payload, signature, secret) {
  const expectedSignature = generateSignature(payload, secret);
  const providedSignature = signature.replace('sha256=', '');
  
  // Use timing-safe comparison to prevent timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(expectedSignature, 'hex'),
    Buffer.from(providedSignature, 'hex')
  );
}

Signature Headers

POST /webhooks/appmint HTTP/1.1
Host: your-app.com
Content-Type: application/json
X-Appmint-Signature: sha256=a0b1c2d3e4f5...
X-Appmint-Event-Type: user.created
X-Appmint-Event-Id: evt_1234567890
X-Appmint-Delivery: delivery_abcd1234
User-Agent: Appmint-Webhooks/1.0

{
  "id": "evt_1234567890",
  "type": "user.created",
  "data": { ... }
}

IP Whitelisting

// Optional: Restrict webhook requests to Appmint IPs
const APPMINT_IPS = [
  '52.89.214.238',
  '34.218.156.209',
  '52.32.178.7',
  // Add more IPs as provided by Appmint
];

function isValidAppmintIP(clientIP) {
  return APPMINT_IPS.includes(clientIP);
}

// In your webhook handler
app.post('/webhooks/appmint', (req, res) => {
  const clientIP = req.ip || req.connection.remoteAddress;
  
  if (!isValidAppmintIP(clientIP)) {
    return res.status(403).send('Forbidden');
  }
  
  // Continue with webhook processing...
});

Error Handling & Reliability

Retry Mechanism

Appmint automatically retries failed webhook deliveries with exponential backoff:

Retry Schedule:

  1. Immediate retry
  2. 1 minute later
  3. 5 minutes later
  4. 15 minutes later
  5. 1 hour later
  6. 6 hours later
  7. 24 hours later

Retry Conditions:

  • HTTP status codes 5xx (server errors)
  • HTTP status codes 408, 429 (timeout, rate limit)
  • Network timeouts or connection errors
  • DNS resolution failures

Idempotency

Handle duplicate webhook deliveries gracefully:

// Use event ID to prevent duplicate processing
const processedEvents = new Set();

function handleWebhookEvent(event) {
  // Check if we've already processed this event
  if (processedEvents.has(event.id)) {
    console.log(`Event ${event.id} already processed, skipping`);
    return;
  }
  
  try {
    // Process the event
    switch (event.type) {
      case 'user.created':
        // Use upsert operations to handle duplicates
        await User.upsert({
          id: event.data.id,
          email: event.data.email,
          name: event.data.name
        });
        break;
    }
    
    // Mark as processed only after successful handling
    processedEvents.add(event.id);
  } catch (error) {
    console.error(`Error processing event ${event.id}:`, error);
    throw error; // Re-throw to trigger retry
  }
}

Error Responses

Return appropriate HTTP status codes:

app.post('/webhooks/appmint', async (req, res) => {
  try {
    await handleWebhookEvent(req.body);
    res.status(200).send('OK');
  } catch (error) {
    if (error.name === 'ValidationError') {
      // Client error - don't retry
      res.status(400).send('Bad Request');
    } else if (error.name === 'DatabaseConnectionError') {
      // Temporary server error - retry
      res.status(500).send('Internal Server Error');
    } else {
      // Unknown error - retry
      res.status(500).send('Internal Server Error');
    }
  }
});

Testing Webhooks

Local Development

Using ngrok for Local Testing

# Install ngrok
npm install -g ngrok

# Start your local server
node server.js  # Running on port 3000

# In another terminal, create public tunnel
ngrok http 3000

# Use the ngrok URL in webhook configuration
# https://abc123.ngrok.io/webhooks/appmint

Webhook Testing Tool

// Simple webhook testing server
const express = require('express');
const app = express();

app.use(express.json());

app.post('/webhooks/test', (req, res) => {
  console.log('Headers:', req.headers);
  console.log('Body:', JSON.stringify(req.body, null, 2));
  res.status(200).send('OK');
});

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

Webhook Simulator

// Create test webhook events via API
const testEvent = await appmint.webhooks.createTestEvent({
  type: 'user.created',
  data: {
    id: 'test_user_123',
    email: 'test@example.com',
    name: 'Test User'
  }
});

console.log('Test event created:', testEvent.id);

Unit Testing Webhook Handlers

const request = require('supertest');
const app = require('./app'); // Your Express app

describe('Webhook Handler', () => {
  test('should handle user.created event', async () => {
    const payload = {
      id: 'evt_test_123',
      type: 'user.created',
      data: {
        id: 'user_test_456',
        email: 'test@example.com',
        name: 'Test User'
      }
    };
    
    const signature = generateTestSignature(payload);
    
    const response = await request(app)
      .post('/webhooks/appmint')
      .set('X-Appmint-Signature', `sha256=${signature}`)
      .send(payload);
      
    expect(response.status).toBe(200);
    
    // Verify that the user was created in your system
    const user = await User.findById('user_test_456');
    expect(user).toBeTruthy();
    expect(user.email).toBe('test@example.com');
  });
});

Advanced Webhook Patterns

Event Filtering

// Server-side filtering
app.post('/webhooks/appmint', (req, res) => {
  const event = req.body;
  
  // Only process events for premium users
  if (event.type === 'user.created' && event.data.plan === 'premium') {
    handlePremiumUserCreated(event.data);
  }
  
  // Only process high-value payments
  if (event.type === 'payment.succeeded' && event.data.amount >= 10000) {
    handleHighValuePayment(event.data);
  }
  
  res.status(200).send('OK');
});

Event Aggregation

// Batch process similar events
const eventBuffer = [];
const BATCH_SIZE = 10;
const BATCH_TIMEOUT = 5000; // 5 seconds

function bufferEvent(event) {
  eventBuffer.push(event);
  
  if (eventBuffer.length >= BATCH_SIZE) {
    processBatch();
  }
}

function processBatch() {
  if (eventBuffer.length === 0) return;
  
  const batch = [...eventBuffer];
  eventBuffer.length = 0; // Clear buffer
  
  // Group events by type
  const groupedEvents = batch.reduce((groups, event) => {
    if (!groups[event.type]) groups[event.type] = [];
    groups[event.type].push(event);
    return groups;
  }, {});
  
  // Process each group
  Object.entries(groupedEvents).forEach(([type, events]) => {
    switch (type) {
      case 'document.created':
        handleBulkDocumentCreation(events);
        break;
      case 'user.updated':
        handleBulkUserUpdates(events);
        break;
    }
  });
}

// Process batch every 5 seconds
setInterval(processBatch, BATCH_TIMEOUT);

Webhook Forwarding

// Forward webhooks to multiple internal services
const services = [
  { name: 'analytics', url: 'http://analytics-service/webhooks' },
  { name: 'notifications', url: 'http://notification-service/webhooks' },
  { name: 'crm', url: 'http://crm-service/webhooks' }
];

async function forwardWebhook(event) {
  const forwardPromises = services.map(async (service) => {
    try {
      await axios.post(service.url, event, {
        headers: { 'Content-Type': 'application/json' },
        timeout: 5000
      });
      console.log(`Forwarded to ${service.name}`);
    } catch (error) {
      console.error(`Failed to forward to ${service.name}:`, error.message);
    }
  });
  
  await Promise.allSettled(forwardPromises);
}

Webhook Management

Webhook Dashboard

// List all webhooks
const webhooks = await appmint.webhooks.list({
  limit: 10,
  starting_after: 'webhook_xyz'
});

// Update webhook configuration
await appmint.webhooks.update('webhook_abc123', {
  events: ['user.created', 'user.updated', 'payment.succeeded'],
  url: 'https://new-url.com/webhooks',
  active: true
});

// Delete webhook
await appmint.webhooks.delete('webhook_abc123');

Webhook Logs

// Retrieve webhook delivery logs
const deliveries = await appmint.webhooks.deliveries('webhook_abc123', {
  limit: 50,
  created: {
    gte: '2024-01-01T00:00:00Z',
    lte: '2024-01-31T23:59:59Z'
  }
});

// Redeliver a failed webhook
await appmint.webhooks.redeliver('delivery_def456');

// Get delivery details
const delivery = await appmint.webhooks.delivery('delivery_def456');
console.log('Status:', delivery.status);
console.log('Response code:', delivery.response_code);
console.log('Response body:', delivery.response_body);

Monitoring & Alerting

// Monitor webhook health
const webhookHealth = {
  totalDeliveries: 0,
  successfulDeliveries: 0,
  failedDeliveries: 0,
  averageResponseTime: 0
};

function trackWebhookMetrics(delivery) {
  webhookHealth.totalDeliveries++;
  
  if (delivery.status === 'succeeded') {
    webhookHealth.successfulDeliveries++;
  } else {
    webhookHealth.failedDeliveries++;
    
    // Alert on high failure rate
    const failureRate = webhookHealth.failedDeliveries / webhookHealth.totalDeliveries;
    if (failureRate > 0.1) { // 10% failure rate
      alerting.send({
        message: `Webhook failure rate is ${(failureRate * 100).toFixed(1)}%`,
        severity: 'warning'
      });
    }
  }
  
  // Calculate average response time
  webhookHealth.averageResponseTime = 
    (webhookHealth.averageResponseTime * (webhookHealth.totalDeliveries - 1) + 
     delivery.response_time) / webhookHealth.totalDeliveries;
}

Best Practices

Performance Optimization

// Async processing for heavy operations
app.post('/webhooks/appmint', async (req, res) => {
  const event = req.body;
  
  // Respond quickly to avoid timeouts
  res.status(200).send('OK');
  
  // Process event asynchronously
  setImmediate(async () => {
    try {
      await processWebhookEvent(event);
    } catch (error) {
      console.error('Async webhook processing failed:', error);
      // Log error for later analysis
      await logWebhookError(event.id, error);
    }
  });
});

// Use queues for reliable processing
const Queue = require('bull');
const webhookQueue = new Queue('webhook processing');

app.post('/webhooks/appmint', (req, res) => {
  // Add to queue for processing
  webhookQueue.add('process-webhook', req.body, {
    attempts: 3,
    backoff: 'exponential'
  });
  
  res.status(200).send('OK');
});

webhookQueue.process('process-webhook', async (job) => {
  const event = job.data;
  await processWebhookEvent(event);
});

Security Checklist

  • Verify signatures - Always validate webhook signatures
  • Use HTTPS - Encrypt webhook traffic
  • IP whitelisting - Restrict to Appmint IPs (optional)
  • Rate limiting - Prevent abuse of webhook endpoints
  • Input validation - Validate webhook payload structure
  • Idempotency - Handle duplicate events gracefully
  • Error logging - Log webhook processing errors
  • Monitoring - Monitor webhook delivery success rates

Troubleshooting

Common Issues

Webhook Not Receiving Events

  1. Verify webhook URL is accessible from internet
  2. Check firewall/security group settings
  3. Ensure SSL certificate is valid
  4. Verify webhook is active in dashboard

Signature Verification Failing

  1. Check webhook secret is correct
  2. Use raw request body for signature verification
  3. Ensure proper encoding (UTF-8)
  4. Verify HMAC SHA-256 implementation

High Failure Rate

  1. Check webhook endpoint response time (<10 seconds)
  2. Return proper HTTP status codes
  3. Handle errors gracefully
  4. Implement proper logging

Debug Tools

// Debug webhook signatures
function debugSignature(payload, providedSignature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(payload, 'utf8')
    .digest('hex');
    
  console.log('Payload length:', payload.length);
  console.log('Expected signature:', expectedSignature);
  console.log('Provided signature:', providedSignature);
  console.log('Signatures match:', expectedSignature === providedSignature.replace('sha256=', ''));
}

// Test webhook endpoint
curl -X POST https://your-app.com/webhooks/appmint \
  -H "Content-Type: application/json" \
  -H "X-Appmint-Signature: sha256=test" \
  -d '{"id":"evt_test","type":"test","data":{}}'

Ready to implement webhooks? Start by setting up your first webhook endpoint and gradually add more events as your application grows.

View Webhook Dashboard → Test Webhook Endpoint → Join Developer Community →