Skip to main content

Overview

Urban Things uses an event-driven architecture with webhooks to notify your applications about important events in real-time.

How It Works

1

Subscribe to Events

Create a webhook subscription for the events you want to receive
2

Event Occurs

When an event happens (e.g., user registered, order created)
3

Outbox Pattern

Event is recorded in the integration_outbox table for reliability
4

Background Processing

A background job processes the event and sends it to your webhook URL
5

Delivery Tracking

Delivery status is tracked in webhook_deliveries table
6

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"]
  }'

Webhook Payload Format

All webhook payloads follow this structure:
{
  "event": "user.registered",
  "tenant_id": 123,
  "timestamp": "2025-11-16T10:30:00Z",
  "data": {
    "user_id": 42,
    "email": "user@example.com",
    "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": "john@example.com",
    "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