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 registrationuser.updated
- User profile changesuser.deleted
- User account deletionuser.login
- User login activityuser.logout
- User logout activitypassword.reset
- Password reset request
Database Events:
document.created
- New document in collectiondocument.updated
- Document modificationdocument.deleted
- Document deletioncollection.created
- New collection createdcollection.deleted
- Collection removed
File Storage Events:
file.uploaded
- New file uploadfile.deleted
- File deletionfile.processing.completed
- File processing finishedfile.processing.failed
- File processing error
Payment Events:
payment.succeeded
- Successful paymentpayment.failed
- Failed paymentpayment.refunded
- Payment refundsubscription.created
- New subscriptionsubscription.updated
- Subscription changessubscription.cancelled
- Subscription cancellation
Workflow Events:
workflow.started
- Workflow execution startedworkflow.completed
- Workflow execution completedworkflow.failed
- Workflow execution failedtask.created
- New task createdtask.completed
- Task completion
System Events:
application.deployed
- New deploymentapplication.error
- Application errorapi.rate_limit_exceeded
- Rate limit reachedbackup.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 identifiertype
- Event type (e.g., 'user.created')created
- ISO 8601 timestamp when event was createdapi_version
- API version when event was generateddata
- Event-specific data payloadprevious_attributes
- Previous values for update eventsrequest
- Information about the API request that triggered the eventlivemode
- 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:
- Immediate retry
- 1 minute later
- 5 minutes later
- 15 minutes later
- 1 hour later
- 6 hours later
- 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
- Verify webhook URL is accessible from internet
- Check firewall/security group settings
- Ensure SSL certificate is valid
- Verify webhook is active in dashboard
Signature Verification Failing
- Check webhook secret is correct
- Use raw request body for signature verification
- Ensure proper encoding (UTF-8)
- Verify HMAC SHA-256 implementation
High Failure Rate
- Check webhook endpoint response time (<10 seconds)
- Return proper HTTP status codes
- Handle errors gracefully
- 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 →