Overview
Urban Things uses an event-driven architecture with webhooks to notify your applications about important events in real-time.
How It Works
Subscribe to Events
Create a webhook subscription for the events you want to receive
Event Occurs
When an event happens (e.g., user registered, order created)
Outbox Pattern
Event is recorded in the integration_outbox table for reliability
Background Processing
A background job processes the event and sends it to your webhook URL
Delivery Tracking
Delivery status is tracked in webhook_deliveries table
Retry Logic
Failed deliveries are retried with exponential backoff
Available Events
User Events
user.registered - New user added to tenant
user.updated - User information changed
user.removed - User removed from tenant
Order Events
order.created - New order placed
order.updated - Order status changed
order.completed - Order fulfilled
order.cancelled - Order cancelled
Product Events
product.created - New product added
product.updated - Product information changed
product.deleted - Product removed
Category Events
category.created - New category added
category.updated - Category information changed
category.deleted - Category removed
Creating a Webhook
Step 1: Prepare Your Endpoint
Create an HTTPS endpoint that can receive POST requests:
// Express.js example
app.post('/webhooks/urban-things', (req, res) => {
const { event, tenant_id, timestamp, data } = req.body;
// Verify signature (recommended)
// Process event
// Respond quickly (within 5 seconds)
res.status(200).json({ received: true });
});
Step 2: Subscribe to Events
curl -X POST \
https://faisalshop.mvp-apps.ae/api/v2/admin/webhooks/123 \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"url": "https://your-app.com/webhooks/urban-things",
"events": ["user.registered", "order.created"]
}'
All webhook payloads follow this structure:
{
"event": "user.registered",
"tenant_id": 123,
"timestamp": "2025-11-16T10:30:00Z",
"data": {
"user_id": 42,
"email": "[email protected]",
"role": "MEMBER",
"invited_by": 17
}
}
Event Payload Examples
User Registered
{
"event": "user.registered",
"tenant_id": 123,
"timestamp": "2025-11-16T10:30:00Z",
"data": {
"user_id": 42,
"name": "John Doe",
"email": "[email protected]",
"role": "MEMBER",
"invited_by": 17
}
}
Order Created
{
"event": "order.created",
"tenant_id": 123,
"timestamp": "2025-11-16T10:30:00Z",
"data": {
"order_id": 1001,
"user_id": 42,
"total": 299.99,
"items_count": 3,
"status": "pending"
}
}
Product Updated
{
"event": "product.updated",
"tenant_id": 123,
"timestamp": "2025-11-16T10:30:00Z",
"data": {
"product_id": 55,
"name": "Wireless Headphones",
"price": 199.99,
"stock": 45,
"changed_fields": ["price", "stock"]
}
}
Security Best Practices
1. Use HTTPS Only
Always use HTTPS URLs for webhook endpoints. HTTP URLs are not secure and may be rejected.
# ✅ Good
"url": "https://your-app.com/webhooks"
# ❌ Bad
"url": "http://your-app.com/webhooks"
2. Verify Webhook Signatures
Verify that requests are actually from Urban Things:
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
app.post('/webhooks', (req, res) => {
const signature = req.headers['x-webhook-signature'];
if (!verifyWebhookSignature(req.body, signature, WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process webhook
res.status(200).json({ received: true });
});
3. Implement Idempotency
Handle duplicate deliveries gracefully:
const processedEvents = new Set();
app.post('/webhooks', async (req, res) => {
const eventId = req.headers['x-event-id'];
// Check if already processed
if (processedEvents.has(eventId)) {
return res.status(200).json({ received: true, duplicate: true });
}
// Process event
await processEvent(req.body);
// Mark as processed
processedEvents.add(eventId);
res.status(200).json({ received: true });
});
4. Respond Quickly
Respond within 5 seconds to avoid timeouts. Process heavy tasks asynchronously.
app.post('/webhooks', async (req, res) => {
// Respond immediately
res.status(200).json({ received: true });
// Process asynchronously
processEventAsync(req.body).catch(console.error);
});
Retry Logic
Failed webhook deliveries are automatically retried:
Retry Schedule
- 1st retry: 1 minute
- 2nd retry: 5 minutes
- 3rd retry: 15 minutes
- 4th retry: 1 hour
- 5th retry: 6 hours
Failure Conditions
- HTTP status 5xx
- Connection timeout
- DNS resolution failure
- SSL/TLS errors
Monitoring Deliveries
Check webhook delivery status:
curl -X GET \
https://faisalshop.mvp-apps.ae/api/v2/admin/webhooks/123/deliveries \
-H 'Authorization: Bearer YOUR_TOKEN'
Response includes:
- Delivery attempts
- Success/failure status
- Response codes
- Timestamps
- Error messages
Testing Webhooks
Local Development
Use tools like ngrok to expose your local server:
# Start ngrok
ngrok http 3000
# Use the ngrok URL for webhook
https://abc123.ngrok.io/webhooks
Test Events
Trigger test events manually:
curl -X POST \
https://faisalshop.mvp-apps.ae/api/v2/admin/webhooks/123/test \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"event": "user.registered"
}'
Common Issues
Webhook Not Receiving Events
Ensure your webhook URL is publicly accessible and returns 200 OK
HTTP URLs may be rejected. Use HTTPS with valid SSL certificate
Ensure your firewall allows incoming requests from Urban Things
Check webhook_deliveries table for error messages
Duplicate Events
This is normal behavior. Implement idempotency to handle duplicates:
// Store processed event IDs in database
const isProcessed = await db.webhookEvents.exists({ eventId });
if (isProcessed) {
return res.status(200).json({ received: true });
}
Slow Processing
Move heavy processing to background jobs:
app.post('/webhooks', async (req, res) => {
// Queue for background processing
await queue.add('process-webhook', req.body);
// Respond immediately
res.status(200).json({ received: true });
});
Best Practices
Respond Fast
Always respond within 5 seconds, process asynchronously
Verify Signatures
Validate webhook signatures to ensure authenticity
Handle Duplicates
Implement idempotency using event IDs
Log Everything
Keep detailed logs for debugging and monitoring
Use HTTPS
Only use HTTPS endpoints with valid certificates
Monitor Failures
Set up alerts for webhook delivery failures
Example Implementation
Complete webhook handler example:
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
// Store processed events (use database in production)
const processedEvents = new Set();
app.post('/webhooks/urban-things', async (req, res) => {
try {
// 1. Verify signature
const signature = req.headers['x-webhook-signature'];
if (!verifySignature(req.body, signature)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// 2. Check for duplicates
const eventId = req.headers['x-event-id'];
if (processedEvents.has(eventId)) {
return res.status(200).json({ received: true, duplicate: true });
}
// 3. Respond immediately
res.status(200).json({ received: true });
// 4. Process asynchronously
processWebhookAsync(req.body, eventId).catch(console.error);
} catch (error) {
console.error('Webhook error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
async function processWebhookAsync(payload, eventId) {
const { event, tenant_id, data } = payload;
// Process based on event type
switch (event) {
case 'user.registered':
await handleUserRegistered(tenant_id, data);
break;
case 'order.created':
await handleOrderCreated(tenant_id, data);
break;
// ... other events
}
// Mark as processed
processedEvents.add(eventId);
}
app.listen(3000, () => {
console.log('Webhook server running on port 3000');
});
Need Help?
Contact Support
Having issues with webhooks? Contact our support team