Skip to main content

Webhook Quick Start Guide

Set up your first EdenPay webhook endpoint in under 5 minutes.

Share
Last updated on August 20, 2026

Set up your first EdenPay webhook endpoint in under 5 minutes.

Prerequisites

  • Active bex.co account (sign up here)
  • A publicly accessible HTTPS server endpoint
  • Basic knowledge of your backend framework

Step 1: Create a Webhook Endpoint (2 minutes)

  1. Log in to your bex.co dashboard
  2. Navigate to Accept PaymentsWebhooks tab
  3. Click "Add Endpoint" button
  4. Configure your endpoint:
    • URL: Enter your HTTPS webhook URL (e.g., https://your-domain.com/api/webhooks)
    • Events: Select events to receive (start with payment.confirmed and payment.failed)
    • Description: Optional description for your reference
  5. Click "Create Endpoint"
  6. Copy the secret displayed in the success modal Save it securely - you won't see it again
Store Your Secret Securely

Save the webhook secret in your environment variables or secrets manager immediately. You'll need it to verify webhook signatures.

Step 2: Implement Your Webhook Handler (3 minutes)

Choose your backend framework:

Node.js with Express

javascript
const express = require('express');
const crypto = require('crypto');
 
const app = express();
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET; // Your webhook secret
 
// IMPORTANT: Use raw body for signature verification
app.post('/api/webhooks',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    // 1. Get signature from headers
    const signature = req.headers['x-eden-signature'];
 
    // 2. Verify signature
    const expectedSignature = crypto
      .createHmac('sha256', WEBHOOK_SECRET)
      .update(req.body)
      .digest('hex');
 
    if (signature !== expectedSignature) {
      console.error('<ion-icon name="warning" style={{verticalAlign: "middle", fontSize: "1.2em", color: "#f59e0b"}}></ion-icon> Invalid signature');
      return res.status(401).send('Unauthorized');
    }
 
    // 3. Parse and handle the event
    const event = JSON.parse(req.body.toString());
    console.log('<ion-icon name="checkmark-circle" style={{verticalAlign: "middle", fontSize: "1.2em", color: "#10b981"}}></ion-icon> Received event:', event.type);
 
    switch (event.type) {
      case 'payment.confirmed':
        // Payment confirmed - safe to fulfill order
        console.log('<ion-icon name="cash" style={{verticalAlign: "middle", fontSize: "1.2em", color: "#10b981"}}></ion-icon> Payment confirmed:', event.data.paymentId);
        // TODO: Your fulfillment logic here
        break;
 
      case 'payment.failed':
        // Payment failed - notify customer
        console.log('<ion-icon name="close-circle" style={{verticalAlign: "middle", fontSize: "1.2em", color: "#ef4444"}}></ion-icon> Payment failed:', event.data.paymentId);
        // TODO: Your failure handling logic here
        break;
 
      default:
        console.log('<ion-icon name="information-circle" style={{verticalAlign: "middle", fontSize: "1.2em", color: "#3b82f6"}}></ion-icon> Unhandled event type:', event.type);
    }
 
    // 4. Return 200 to acknowledge receipt
    res.status(200).json({ received: true });
  }
);
 
app.listen(3000, () => {
  console.log('<ion-icon name="headset" style={{verticalAlign: "middle", fontSize: "1.2em", color: "#8b5cf6"}}></ion-icon> Webhook server listening on port 3000');
});

Next.js App Router

typescript
// app/api/webhooks/route.ts
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';
 
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET!;
 
export async function POST(req: NextRequest) {
  try {
    // 1. Get signature from headers
    const signature = req.headers.get('x-eden-signature');
    if (!signature) {
      return NextResponse.json(
        { error: 'Missing signature' },
        { status: 401 }
      );
    }
 
    // 2. Get raw body for signature verification
    const body = await req.text();
 
    // 3. Verify signature
    const expectedSignature = crypto
      .createHmac('sha256', WEBHOOK_SECRET)
      .update(body)
      .digest('hex');
 
    if (signature !== expectedSignature) {
      console.error('<ion-icon name="warning" style={{verticalAlign: "middle", fontSize: "1.2em", color: "#f59e0b"}}></ion-icon> Invalid signature');
      return NextResponse.json(
        { error: 'Invalid signature' },
        { status: 401 }
      );
    }
 
    // 4. Parse and handle the event
    const event = JSON.parse(body);
    console.log('<ion-icon name="checkmark-circle" style={{verticalAlign: "middle", fontSize: "1.2em", color: "#10b981"}}></ion-icon> Received event:', event.type);
 
    switch (event.type) {
      case 'payment.confirmed':
        console.log('<ion-icon name="cash" style={{verticalAlign: "middle", fontSize: "1.2em", color: "#10b981"}}></ion-icon> Payment confirmed:', event.data.paymentId);
        // TODO: Your fulfillment logic here
        await fulfillOrder(event.data);
        break;
 
      case 'payment.failed':
        console.log('<ion-icon name="close-circle" style={{verticalAlign: "middle", fontSize: "1.2em", color: "#ef4444"}}></ion-icon> Payment failed:', event.data.paymentId);
        // TODO: Your failure handling logic here
        await handleFailedPayment(event.data);
        break;
    }
 
    // 5. Return 200 to acknowledge receipt
    return NextResponse.json({ received: true }, { status: 200 });
 
  } catch (error) {
    console.error('Error processing webhook:', error);
    return NextResponse.json(
      { error: 'Webhook processing failed' },
      { status: 500 }
    );
  }
}
 
// Example fulfillment function
async function fulfillOrder(paymentData: any) {
  // Your business logic here
  // - Update database
  // - Send confirmation email
  // - Grant access to purchased content
}
 
async function handleFailedPayment(paymentData: any) {
  // Your failure handling logic here
  // - Send failure notification
  // - Update order status
}

Python with Flask

python
from flask import Flask, request, jsonify
import hmac
import hashlib
import os
 
app = Flask(__name__)
WEBHOOK_SECRET = os.environ.get('WEBHOOK_SECRET')
 
@app.route('/api/webhooks', methods=['POST'])
def webhook():
    # 1. Get signature from headers
    signature = request.headers.get('x-eden-signature')
 
    # 2. Verify signature
    body = request.get_data()
    expected_signature = hmac.new(
        WEBHOOK_SECRET.encode('utf-8'),
        body,
        hashlib.sha256
    ).hexdigest()
 
    if signature != expected_signature:
        print('<ion-icon name="warning" style={{verticalAlign: "middle", fontSize: "1.2em", color: "#f59e0b"}}></ion-icon> Invalid signature')
        return jsonify({'error': 'Unauthorized'}), 401
 
    # 3. Parse and handle the event
    event = request.get_json()
    print(f'<ion-icon name="checkmark-circle" style={{verticalAlign: "middle", fontSize: "1.2em", color: "#10b981"}}></ion-icon> Received event: {event["type"]}')
 
    if event['type'] == 'payment.confirmed':
        print(f'<ion-icon name="cash" style={{verticalAlign: "middle", fontSize: "1.2em", color: "#10b981"}}></ion-icon> Payment confirmed: {event["data"]["paymentId"]}')
        # TODO: Your fulfillment logic here
 
    elif event['type'] == 'payment.failed':
        print(f'<ion-icon name="close-circle" style={{verticalAlign: "middle", fontSize: "1.2em", color: "#ef4444"}}></ion-icon> Payment failed: {event["data"]["paymentId"]}')
        # TODO: Your failure handling logic here
 
    # 4. Return 200 to acknowledge receipt
    return jsonify({'received': True}), 200
 
if __name__ == '__main__':
    app.run(port=3000)

Step 3: Test Your Webhook

  1. Deploy your webhook handler to your server
  2. Go back to the bex.co dashboard → Webhooks tab
  3. Click "Test" on your webhook endpoint
  4. Check your server logs to verify:
    • Event was received
    • Signature was validated correctly
    • Event was processed successfully
Testing Locally

For local development, use tools like ngrok or localtunnel to expose your local server to the internet:

bash
# With ngrok
ngrok http 3000
 
# With localtunnel
npx localtunnel --port 3000

Step 4: Handle Webhook Delivery

Return 200 Quickly

Your endpoint must return a 200 OK response within 30 seconds. If processing takes longer, acknowledge receipt immediately and process asynchronously:

javascript
app.post('/api/webhooks', async (req, res) => {
  // Verify signature...
 
  // Return 200 immediately
  res.status(200).json({ received: true });
 
  // Process asynchronously
  processWebhookAsync(event).catch(console.error);
});
 
async function processWebhookAsync(event) {
  // Your heavy processing here
  // - Database operations
  // - External API calls
  // - Send emails
}

Monitor Delivery Attempts

View delivery attempts in the dashboard to:

  • See request/response details
  • Check error messages
  • Replay failed events
  • Monitor success rate

Security Checklist

  • Always verify signatures before processing events
  • Store secrets securely in environment variables
  • Use HTTPS only - HTTP endpoints will be rejected
  • Return 200 quickly - process heavy tasks asynchronously
  • Handle idempotency - events may be delivered more than once
  • Validate event structure before processing

Common Issues

401 Unauthorized

Cause: Signature verification is failing

Solutions:

  • Verify you're using the correct webhook secret
  • Ensure you're hashing the raw request body (before parsing)
  • Check that your HMAC implementation matches (SHA-256)

Webhook Timeout

Cause: Your endpoint took longer than 30 seconds to respond

Solutions:

  • Return 200 immediately and process asynchronously
  • Move database operations to background queue
  • Optimize slow API calls

Missing Events

Cause: Your endpoint is not publicly accessible

Solutions:

  • Verify your endpoint is reachable from the internet
  • Check firewall rules and security groups
  • Test with curl from an external server

Next Steps

Need Help?

  • View Logs: Check delivery attempts in the dashboard
  • Replay Events: Use the "Replay" button for failed deliveries
  • Forum: https://bex.co/forum
  • Support: hello@bex.co

Build on bex.co

Open-source infrastructure for apps and agents — deploy on machines you own, or use the hosted multi-chain API.

See pricing