Integrations
Webhooks

Webhooks

Webhooks let your application receive real-time notifications when things happen in your BareCommerceCore store.

Important: Inbound vs Outbound Webhooks

BareCommerce works with two types of webhooks:

Inbound Webhooks (Payment Providers → BareCommerce)

When customers pay via Stripe, PayPal, or Square, those payment providers send webhooks to BareCommerce. These webhooks create orders automatically in BareCommerce. See payment guides for setup details.

Outbound Webhooks (BareCommerce → Your App)

When orders, products, or customers are created/updated in BareCommerce, BareCommerce sends webhooks to YOUR endpoint. This guide covers outbound webhooks.


How Outbound Webhooks Work

Flow:

  1. Something happens in BareCommerceCore (order created, product updated, etc.)
  2. BareCommerceCore sends an HTTP POST to your webhook URL
  3. Your app receives the event and takes action
  4. Your app returns a success response (200 OK)
  5. BareCommerce confirms delivery

If you don't return 200 OK, BareCommerce retries with exponential backoff.

Setting Up a Webhook

1. Create an Endpoint

Set up an HTTPS endpoint that can receive POST requests:

Next.js App Router:

// app/api/webhooks/barecommerce/route.ts
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';
 
export async function POST(request: NextRequest) {
  try {
    // Get signature from headers
    const signature = request.headers.get('x-webhook-signature');
    const body = await request.text();
    
    // Verify signature (see below)
    if (!verifySignature(body, signature!, process.env.WEBHOOK_SECRET!)) {
      return NextResponse.json(
        { error: 'Invalid signature' },
        { status: 401 }
      );
    }
    
    // Parse event
    const event = JSON.parse(body);
    
    // Handle different event types
    switch (event.type) {
      case 'order.created':
        await handleOrderCreated(event.data);
        break;
      case 'order.updated':
        await handleOrderUpdated(event.data);
        break;
      case 'product.created':
        await handleProductCreated(event.data);
        break;
      // ... more event handlers
    }
    
    // Return success immediately
    return NextResponse.json({ ok: true });
  } catch (error) {
    console.error('Webhook error:', error);
    // Return 500 to trigger retry
    return NextResponse.json(
      { error: 'Processing failed' },
      { status: 500 }
    );
  }
}
 
async function handleOrderCreated(order: any) {
  console.log(`New order ${order.orderNumber}:`, order.total);
  // Send confirmation email
  // Update inventory
  // Notify warehouse
  // etc.
}
 
async function handleOrderUpdated(order: any) {
  console.log(`Order ${order.orderNumber} updated:`, order.status);
  // Notify customer of status change
}
 
async function handleProductCreated(product: any) {
  console.log(`Product created: ${product.title}`);
  // Sync to external system
}
 
function verifySignature(body: string, signature: string, secret: string): boolean {
  const computed = crypto
    .createHmac('sha256', secret)
    .update(body)
    .digest('hex');
  
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(computed)
  );
}

Express.js:

// webhooks/barecommerce.ts
import express from 'express';
import crypto from 'crypto';
 
const router = express.Router();
 
// Middleware to capture raw body for signature verification
router.use(express.raw({type: 'application/json'}));
 
router.post('/webhooks/barecommerce', async (req, res) => {
  try {
    const signature = req.headers['x-webhook-signature'] as string;
    const body = req.body as Buffer;
    
    if (!verifySignature(body.toString(), signature, process.env.WEBHOOK_SECRET!)) {
      return res.status(401).json({ error: 'Invalid signature' });
    }
    
    const event = JSON.parse(body.toString());
    
    // Process in background (return immediately)
    processEvent(event).catch(err => {
      console.error('Event processing failed:', err);
    });
    
    res.status(200).json({ ok: true });
  } catch (error) {
    console.error('Webhook error:', error);
    res.status(500).json({ error: 'Processing failed' });
  }
});
 
function verifySignature(body: string, signature: string, secret: string): boolean {
  const computed = crypto
    .createHmac('sha256', secret)
    .update(body)
    .digest('hex');
  
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(computed)
  );
}
 
async function processEvent(event: any) {
  // Handle events asynchronously
}

2. Create a Webhook Subscription

Go to your BareCommerceCore dashboard:

  1. Navigate to Store SettingsWebhooks
  2. Click Create Webhook
  3. Enter your endpoint URL (must be HTTPS)
  4. Select events to subscribe to (or subscribe to all)
  5. Click Create
  6. Copy the Signing Secret and store it in your .env:
WEBHOOK_SECRET=whsec_xxxxxxxxxxxxx

3. Verify the Signature

Critical: Always verify webhook signatures. Don't trust unsigned webhooks.

function verifySignature(body: string, signature: string, secret: string): boolean {
  // Create HMAC-SHA256 hash of body using secret
  const computed = crypto
    .createHmac('sha256', secret)
    .update(body)
    .digest('hex');
  
  // Compare using timing-safe comparison
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(computed)
  );
}

Important: Use the raw request body (string), not parsed JSON. The signature is computed on the exact bytes sent.

4. Implement Idempotency

BareCommerce may retry webhooks if your endpoint doesn't respond with 200. Handle duplicate deliveries:

import { prisma } from '@/lib/prisma';
 
async function processEvent(event: any) {
  // Check if already processed
  const processed = await prisma.webhookEvent.findUnique({
    where: {
      idempotencyKey: event.id // Unique event ID from webhook
    }
  });
  
  if (processed) {
    console.log('Duplicate webhook, skipping');
    return;
  }
  
  try {
    // Process the event
    if (event.type === 'order.created') {
      await handleOrderCreated(event.data);
    }
    
    // Mark as processed
    await prisma.webhookEvent.create({
      data: {
        idempotencyKey: event.id,
        eventType: event.type,
        resourceId: event.data.id,
        processedAt: new Date()
      }
    });
  } catch (error) {
    // Don't mark as processed - let retry happen
    throw error;
  }
}

Available Events

Orders

  • order.created — Order created from payment webhook

    {
      "type": "order.created",
      "id": "evt_123",
      "data": {
        "id": "order_123",
        "orderNumber": "ORD-000001",
        "customerId": "cust_123",
        "total": "99.99",
        "status": "paid",
        "paymentStatus": "succeeded",
        "items": [...],
        "shippingAddress": {...}
      }
    }
  • order.updated — Order status or data changed

  • order.paid — Payment confirmed (separate from order.created)

  • order.fulfilled — Order was shipped

  • order.cancelled — Order was cancelled

  • order.refunded — Order was refunded

Products

  • product.created — Product was created
  • product.updated — Product was updated
  • product.deleted — Product was deleted

Customers

  • customer.created — Customer was created
  • customer.updated — Customer was updated
  • customer.deleted — Customer was deleted

Other

  • page.created, page.updated, page.deleted
  • category.created, category.updated, category.deleted
  • media.created, media.updated, media.deleted

Webhook Payload Format

All webhooks follow this structure:

{
  "type": "order.created",
  "id": "evt_abc123",
  "timestamp": "2024-01-15T10:30:00Z",
  "storeId": "store_xyz",
  "data": {
    "id": "resource_id",
    ...resource fields...
  }
}
FieldTypeDescription
typestringEvent type (e.g., "order.created")
idstringUnique event ID (use for idempotency)
timestampdatetimeWhen event occurred
storeIdstringWhich store the event is from
dataobjectThe actual resource (order, product, etc.)

Best Practices

✅ Return 200 Immediately

Don't keep BareCommerce waiting. Return 200 OK as soon as you receive the webhook, even if you're still processing:

app.post('/webhooks/barecommerce', async (req, res) => {
  // Verify signature first
  if (!isValidSignature(req)) {
    return res.status(401).json({ error: 'Invalid' });
  }
  
  // Return 200 immediately
  res.status(200).json({ ok: true });
  
  // Process in background
  const event = JSON.parse(req.body);
  processEvent(event).catch(err => {
    console.error('Failed to process:', err);
    // Send alert
  });
});

✅ Always Verify Signatures

// ✅ DO THIS
const isValid = verifySignature(body, signature, secret);
if (!isValid) return res.status(401).json({error: 'Invalid'});
 
// ❌ DON'T DO THIS
// Trust webhook just because it arrived

✅ Implement Idempotency

Handle the same webhook being delivered twice:

// ✅ Track processed events
const eventKey = `${event.type}:${event.id}`;
if (await db.isProcessed(eventKey)) return;
 
// Process event...
 
await db.markProcessed(eventKey);

✅ Log Everything

Log all webhook activity for debugging:

console.log(`[${event.type}] ${event.id} for ${event.storeId}`);
console.log(`Data:`, JSON.stringify(event.data, null, 2));

✅ Handle Errors Gracefully

try {
  // Process event
} catch (error) {
  console.error('Error processing webhook:', error);
  // Return 500 to trigger retry
  return res.status(500).json({ error: error.message });
}

❌ Common Mistakes

  • Don't parse JSON body for signature verification (use raw bytes)
  • Don't forget to add X-Webhook-Signature header verification
  • Don't process the same event twice
  • Don't make BareCommerce wait while you process

Testing Webhooks

1. Local Testing with ngrok

# Install ngrok
brew install ngrok
 
# Expose your local server
ngrok http 3000
 
# Your URL: https://abc123.ngrok.io/api/webhooks/barecommerce

2. Test with cURL

curl -X POST http://localhost:3000/api/webhooks/barecommerce \
  -H "Content-Type: application/json" \
  -H "X-Webhook-Signature: sha256=test_signature" \
  -d '{
    "type": "order.created",
    "id": "evt_test",
    "data": {"id": "order_1", "orderNumber": "ORD-001", "total": "99.99"}
  }'

3. Dashboard Test Events

In BareCommerceCore dashboard, go to Webhooks → [Your Webhook] → "Test" to send a real test event.

Webhook Retry Logic

If your endpoint returns an error (5xx) or doesn't respond, BareCommerce retries with exponential backoff:

  • Attempt 1: Immediately
  • Attempt 2: 5 seconds later
  • Attempt 3: 25 seconds later
  • Attempt 4: 2 minutes later
  • Attempt 5: 10 minutes later

Return 200 OK to confirm successful delivery.

Environment Variables

# Store in .env.local
WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxx
WEBHOOK_URL=https://yourapp.com/api/webhooks/barecommerce

Next Steps