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:
- Something happens in BareCommerceCore (order created, product updated, etc.)
- BareCommerceCore sends an HTTP POST to your webhook URL
- Your app receives the event and takes action
- Your app returns a success response (200 OK)
- 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:
- Navigate to Store Settings → Webhooks
- Click Create Webhook
- Enter your endpoint URL (must be HTTPS)
- Select events to subscribe to (or subscribe to all)
- Click Create
- Copy the Signing Secret and store it in your
.env:
WEBHOOK_SECRET=whsec_xxxxxxxxxxxxx3. 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 createdproduct.updated— Product was updatedproduct.deleted— Product was deleted
Customers
customer.created— Customer was createdcustomer.updated— Customer was updatedcustomer.deleted— Customer was deleted
Other
page.created,page.updated,page.deletedcategory.created,category.updated,category.deletedmedia.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...
}
}| Field | Type | Description |
|---|---|---|
type | string | Event type (e.g., "order.created") |
id | string | Unique event ID (use for idempotency) |
timestamp | datetime | When event occurred |
storeId | string | Which store the event is from |
data | object | The 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-Signatureheader 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/barecommerce2. 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/barecommerceNext Steps
- View the full API Reference
- Check out Payment Webhooks (inbound webhooks from Stripe/PayPal/Square)
- Check out Best Practices
- Join the Discord (opens in a new tab) for help