How to: Build a Whatsapp E-commerce Flow - Netlify function create-order-proxy Part 6
File netlify/functions/create-order-proxy.js — lightweight proxy that accepts a minimal payload from the webhook function and forwards to your main backend or runs the order creation logic inline. Drop this file into netlify/functions/. Configure environment variables in Netlify UI as shown in the repo README.
// netlify/functions/create-order-proxy.js
const fetch = require('node-fetch');
const Stripe = require('stripe');
const { Pool } = require('pg');
const Redis = require('ioredis');
const {
BACKEND_MODE = 'proxy', // 'proxy' or 'inline'
BACKEND_URL, // used when BACKEND_MODE=proxy
DATABASE_URL,
REDIS_URL,
STRIPE_SECRET,
BASE_URL,
WHATSAPP_PHONE_NUMBER_ID,
WHATSAPP_ACCESS_TOKEN
} = process.env;
const stripe = STRIPE_SECRET ? Stripe(STRIPE_SECRET) : null;
const pg = DATABASE_URL ? new Pool({ connectionString: DATABASE_URL }) : null;
const redis = REDIS_URL ? new Redis(REDIS_URL) : null;
// Helper: send WhatsApp message via Cloud API
async function sendWhatsAppMessage(toPhone, payload) {
const url = `https://graph.facebook.com/v18.0/${WHATSAPP_PHONE_NUMBER_ID}/messages`;
const res = await fetch(url, {
method: 'POST',
headers: {
Authorization: `Bearer ${WHATSAPP_ACCESS_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(Object.assign({ messaging_product: 'whatsapp', to: toPhone }, payload))
});
return res.json();
}
// Minimal reservation helper using Redis
async function reserveSku(sku, qty, orderId, ttlSeconds = 10 * 60) {
if (!redis) return true;
const key = `reservation:${sku}:${orderId}`;
const exists = await redis.get(key);
if (exists) return false;
await redis.set(key, qty.toString(), 'EX', ttlSeconds);
await redis.sadd(`reservations_by_sku:${sku}`, key);
return true;
}
// Inline order creation logic (simple)
async function createOrderInline({ phone, sku, wa_message_id }) {
if (!pg) throw new Error('DATABASE_URL not configured for inline mode');
const client = await pg.connect();
try {
// lookup product and variant
const q = `
SELECT p.id, p.sku, p.title, p.price_cents, p.currency, v.id AS variant_id, v.stock, v.price_delta_cents
FROM products p
LEFT JOIN variants v ON v.product_id = p.id
WHERE p.sku = $1
LIMIT 1
`;
const r = await client.query(q, [sku]);
if (!r.rows.length) {
return { status: 'not_found', message: `SKU ${sku} not found` };
}
const row = r.rows[0];
const unitPrice = row.price_cents + (row.price_delta_cents || 0);
const items = [{ sku: row.sku, variant_id: row.variant_id, qty: 1, unit_price_cents: unitPrice }];
const subtotal = unitPrice;
const total = subtotal;
const externalId = `wa_${Date.now()}_${Math.random().toString(36).slice(2,8)}`;
const insert = await client.query(
`INSERT INTO orders (external_id, customer_phone, items, subtotal_cents, total_cents, payment_status, metadata)
VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING id`,
[externalId, phone, JSON.stringify(items), subtotal, total, 'pending', JSON.stringify({ wa_message_id })]
);
const orderId = insert.rows[0].id;
// reserve stock
const reserved = await reserveSku(row.sku, 1, orderId, 10 * 60);
if (!reserved) {
// mark order as expired or delete
await client.query('UPDATE orders SET fulfillment_status=$1 WHERE id=$2', ['expired', orderId]);
return { status: 'unavailable', message: 'Item temporarily unavailable' };
}
// create Stripe Checkout session if configured
let checkoutUrl = null;
if (stripe) {
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
mode: 'payment',
line_items: [{
price_data: {
currency: row.currency.toLowerCase(),
product_data: { name: row.title },
unit_amount: unitPrice
},
quantity: 1
}],
metadata: { order_id: orderId },
success_url: `${BASE_URL}/checkout/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${BASE_URL}/checkout/cancel`
});
checkoutUrl = session.url;
}
// send WhatsApp message with checkout link
const messageBody = checkoutUrl
? `Order created for ${row.title}\nTotal: ${(total/100).toFixed(2)} ${row.currency}\nPay here: ${checkoutUrl}`
: `Order created for ${row.title}\nTotal: ${(total/100).toFixed(2)} ${row.currency}\nPlease complete payment via your preferred method.`;
await sendWhatsAppMessage(phone, { type: 'text', text: { body: messageBody } });
return { status: 'created', order_id: orderId, checkout_url: checkoutUrl };
} finally {
client.release();
}
}
exports.handler = async function (event) {
try {
if (event.httpMethod !== 'POST') return { statusCode: 405, body: 'Method Not Allowed' };
const payload = JSON.parse(event.body || '{}');
const phone = payload.phone || payload.from;
const sku = payload.sku;
const wa_message_id = payload.wa_message_id || null;
if (!phone || !sku) return { statusCode: 400, body: JSON.stringify({ error: 'missing phone or sku' }) };
if (BACKEND_MODE === 'proxy') {
if (!BACKEND_URL) return { statusCode: 500, body: JSON.stringify({ error: 'BACKEND_URL not configured' }) };
// forward to main backend
const res = await fetch(`${BACKEND_URL}/create-order`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ customer_phone: phone, items: [{ sku, qty: 1 }], source: 'whatsapp', metadata: { wa_message_id } })
});
const json = await res.json();
return { statusCode: res.status, body: JSON.stringify(json) };
} else {
// inline mode: create order directly in Netlify function
const result = await createOrderInline({ phone, sku, wa_message_id });
return { statusCode: 200, body: JSON.stringify(result) };
}
} catch (err) {
console.error('create-order-proxy error', err);
return { statusCode: 500, body: JSON.stringify({ error: 'server_error', message: err.message }) };
}
};
Netlify functions package ready to drop in
Place these files under netlify/functions/:
webhook.js(from earlier)payment-callback.js(from earlier)create-order-proxy.js(above)
Environment variables to set in Netlify Site settings
Required: DATABASE_URL, REDIS_URL, WHATSAPP_PHONE_NUMBER_ID, WHATSAPP_ACCESS_TOKEN, WHATSAPP_APP_SECRET, VERIFY_TOKEN, STRIPE_SECRET, STRIPE_WEBHOOK_SECRET, BASE_URL, BACKEND_MODE, BACKEND_URL (if using proxy mode).
Notes
- Use
BACKEND_MODE=proxyto forward requests to your main backend (recommended for heavy DB work). - Use
BACKEND_MODE=inlineonly if you configuredDATABASE_URLandREDIS_URLin Netlify and accept function execution time limits. - For Stripe webhooks, register the endpoint at
https://<site>/.netlify/functions/payment-callbackand copy the webhook secret intoSTRIPE_WEBHOOK_SECRET.
Blog ready README snippet for publishing
Title Build a WhatsApp Commerce Flow with Netlify Functions Docker and cPanel
TLDR Copy the repo, set environment variables, run docker compose up --build, import the Postman collection, and test the flow: message → product selection → create order → checkout → payment webhook → WhatsApp confirmation.
Quick copy paste commands
git clone https://github.com/your-org/whatsapp-commerce-starter.git
cd whatsapp-commerce-starter
cp .env.example .env
# edit .env with your secrets
docker compose up --build
# apply DB migration
docker compose exec db psql -U postgres -d wa -f /app/migrations/migration.sql
Netlify drop in
- Copy
netlify/functions/into your repo. - Set environment variables in Netlify Site settings.
- Deploy site.
- Configure Stripe webhook to
https://<site>/.netlify/functions/payment-callback.
Files included
netlify/functions/webhook.js— WhatsApp webhook receiver.netlify/functions/create-order-proxy.js— order creation proxy or inline order creator.netlify/functions/payment-callback.js— Stripe webhook handler.src/index.js— Express app for traditional hosting.Dockerfileanddocker-compose.yml— local test environment.migrations/migration.sql— minimal schema.scripts/cleanup-reservations.jsandscripts/reserve.lua— reservation cleanup and atomic reserve.
Image placeholders for blog
- Screenshot 1: Postman flow showing
Send Product Listrequest. - Screenshot 2: Netlify environment variables panel.
- Screenshot 3: Docker Compose logs showing services up.
Short FAQ
- Why a proxy function — keep heavy DB work off serverless functions to avoid timeouts; proxy to a dedicated backend.
- How to avoid oversell — use Redis reservations with TTL and atomic Lua script for stock decrement.
- WhatsApp templates — submit
order_confirmationandshipping_updatetemplates early for approval.
Final notes and next steps
- I packaged the create-order-proxy function so you can run order creation either inline or forwarded to your backend.
- Next, I will now generate a ready
.env.examplefile tailored for Netlify and a smallREADME_BLOG.mdformatted for your blog with copy‑paste code blocks and image markdown placeholders.
0 Comments