Skip to main content

Webhook Integration

Open Market uses webhooks to notify your application when a payment is successful. Only successful transactions trigger webhook notifications. This real-time notification system allows you to automatically update your application’s state, fulfill orders, or trigger other business processes.

Webhook Authentication

Webhooks are authenticated using your private key. Each webhook request includes a signature in the opm-signature header that you should validate to ensure the webhook is legitimate. This prevents unauthorized parties from sending fake webhook events to your endpoint.

Webhook Payload

When a payment is successful, we’ll send a POST request to your configured webhook URL with the following JSON payload:
{
  "transaction_id": "tr_123456789",
  "reference": "ORDER123ABC",
  "meta_data": {
    "order_id": "ORD_123",
    "customer_id": "CUS_456",
    "product_ids": ["PROD_789", "PROD_012"]
  },
  "status": "completed",
  "transaction_type": "payment",
  "amount": "5000.00",
  "currency": "XOF",
  "buyer_number": "+22961234567",
  "buyer_name": "John Doe",
  "buyer_email": "john@example.com",
  "seller_email": "merchant@example.com",
  "created_at": "2024-01-20T14:30:00Z",
  "updated_at": "2024-01-20T14:32:00Z"
}

Handling Webhooks

Here’s an example of how to handle and validate webhooks:
const crypto = require('crypto');
const express = require('express');
const app = express();

// Middleware to parse JSON bodies
app.use(express.json());

// Utility function to validate signature
function validateSignature(payload, signature, privateKey) {
  const hmac = crypto.createHmac('sha256', privateKey);
  const expectedSignature = hmac.update(JSON.stringify(payload)).digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

// Database model for processed webhooks (example using Mongoose)
const ProcessedWebhook = mongoose.model('ProcessedWebhook', {
  transaction_id: { type: String, unique: true },
  processed_at: { type: Date, default: Date.now }
});

// Webhook handler
app.post('/webhook', async (req, res) => {
  try {
    const signature = req.headers['opm-signature'];
    const payload = req.body;
    const privateKey = process.env.OPM_PRIVATE_KEY;

    // 1. Validate signature
    if (!validateSignature(payload, signature, privateKey)) {
      console.error('Invalid signature for transaction:', payload.transaction_id);
      return res.status(400).send('Invalid signature');
    }

    // 2. Check for duplicate webhook
    const existingWebhook = await ProcessedWebhook.findOne({
      transaction_id: payload.transaction_id
    });

    if (existingWebhook) {
      console.log('Duplicate webhook received:', payload.transaction_id);
      return res.status(200).send('Webhook already processed');
    }

    // 3. Process the webhook asynchronously
    res.status(200).send('Webhook received');

    // 4. Business logic
    await processWebhookData(payload);

    // 5. Mark webhook as processed
    await ProcessedWebhook.create({
      transaction_id: payload.transaction_id
    });

  } catch (error) {
    console.error('Webhook processing error:', error);
    // Still return 200 to acknowledge receipt
    if (!res.headersSent) {
      res.status(200).send('Webhook received with errors');
    }
    
    // Store failed webhook for retry
    await storeFailedWebhook(payload, error);
  }
});

// Business logic processing
async function processWebhookData(payload) {
  const {
    transaction_id,
    reference,
    meta_data,
    status,
    amount,
    buyer_email,
    buyer_name
  } = payload;

  // Update order status
  await Orders.findOneAndUpdate(
    { reference },
    {
      payment_status: status,
      payment_confirmed: true,
      transaction_id
    }
  );

  // Process meta data
  if (meta_data?.order_id) {
    await fulfillOrder(meta_data.order_id);
  }

  // Send confirmation email
  await sendPaymentConfirmation({
    email: buyer_email,
    name: buyer_name,
    amount,
    reference
  });

  // Additional business logic...
}

// Failed webhook storage
async function storeFailedWebhook(payload, error) {
  await FailedWebhooks.create({
    transaction_id: payload.transaction_id,
    payload: payload,
    error: error.message,
    created_at: new Date(),
    retry_count: 0
  });
}

// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Webhook server listening on port ${PORT}`);
});

Best Practices for Webhooks

Always validate the webhook signature using your private key to ensure the request is from Open Market.
Implement idempotency checks using the transaction_id to avoid processing the same webhook multiple times.
Implement proper error handling and logging for webhook processing failures.
Return a 200 status code quickly and process the webhook asynchronously if needed.
Implement a retry mechanism in case your server fails to process the webhook.

Webhook Use Cases

  • Simple payment confirmation
  • Update order status
  • Send confirmation emails
  • Basic logging of transactions
  • Order fulfillment automation
  • Inventory management
  • Customer notification system
  • Basic error handling and retries
  • Simple database storage
  • Distributed systems integration
  • Queue-based processing
  • Advanced error handling with retry mechanisms
  • Load balancing and scaling
  • Monitoring and alerting systems
  • Data analytics and reporting
  • Multi-region deployment
  • Microservices architecture
  • Event-driven systems
  • High availability setup
  • Disaster recovery
  • Compliance and audit logging
  • Advanced security measures
  • Performance optimization

Testing Webhooks

During development, you can use tools like ngrok to test webhooks locally:
ngrok http 3000
Then update your webhook URL in the API Settings with the ngrok URL.
Remember to update your webhook URL to your production URL before going live.