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

Webhook Use Cases

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.