DEV Community

Cover image for Build a WhatsApp Storefront in Google Sheets with OpenAI Function Calling (No Website)
Hayrullah Kar
Hayrullah Kar

Posted on • Originally published at magesheet.com

Build a WhatsApp Storefront in Google Sheets with OpenAI Function Calling (No Website)

A café owner asked me for an online store. She didn't want Shopify, a domain, a theme, or a $90/month bill — she wanted customers to message her on WhatsApp, ask what's available, and place an order. That's not a website. That's three functions and a spreadsheet.

Here's the whole thing: an AI that browses your catalog and logs orders over chat, backed by two Google Sheets tabs, for under $20/month. It's about 200 lines of Apps Script, and the three tools that do the work are below — verbatim.

Why function calling is the right tool here

A keyword bot ("reply 1 for menu") falls apart the moment a customer types "what pastries do you have under 50?". A language model handles that phrasing easily — but on its own it can only talk. To actually read your catalog and write an order, it needs tools: functions it's allowed to call.

OpenAI's function calling is exactly that. You hand GPT a list of tool schemas; when it decides it needs data, it returns a tool_call naming the tool and its arguments; your Apps Script runs the matching function and hands the result back. Three tools cover a storefront: list products, look up one product, log an order.

You need two tabs. Products: sku | name | description | price | currency | stock | active. Orders: orderId | phone | sku | name | qty | price | total | currency | notes | status | timestamp. That's the entire database.

Tool 1: read the catalog

The model calls this when a customer wants to browse. It skips anything inactive or out of stock, and treats a blank stock cell as "unlimited" (handy for services and made-to-order items).

function toolGetProducts(phone, args, lead) {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Products');
  if (!sheet || sheet.getLastRow() < 2) {
    return { products: [], note: 'No products configured yet.' };
  }

  const data = sheet.getDataRange().getValues();
  const products = [];
  for (let i = 1; i < data.length; i++) {
    const [sku, name, desc, price, currency, stock, active] = data[i];
    if (active !== true) continue;
    if (stock !== '' && Number(stock) <= 0) continue;
    products.push({
      sku: String(sku),
      name: String(name),
      description: String(desc || ''),
      price: Number(price),
      currency: String(currency || ''),
      in_stock: stock === '' ? null : Number(stock)
    });
  }
  return { count: products.length, products };
}
Enter fullscreen mode Exit fullscreen mode

Tools 2 & 3: look up a product, then log the order

toolGetProduct is a case-insensitive SKU lookup. toolLogOrder is the one that touches money, so it validates first, re-reads the product for the authoritative price (never trusting a price the model might invent), computes the total itself, and stamps a unique order id.

function toolGetProduct(phone, args, lead) {
  const sku = args && args.sku ? String(args.sku).trim() : '';
  if (!sku) return { error: 'sku is required' };

  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Products');
  const data = sheet.getDataRange().getValues();
  for (let i = 1; i < data.length; i++) {
    const row = data[i];
    if (String(row[0]).toLowerCase() === sku.toLowerCase()) {
      return {
        sku: String(row[0]),
        name: String(row[1]),
        description: String(row[2] || ''),
        price: Number(row[3]),
        currency: String(row[4] || ''),
        in_stock: row[5] === '' ? null : Number(row[5]),
        active: row[6] === true
      };
    }
  }
  return { error: 'Product ' + sku + ' not found' };
}

function toolLogOrder(phone, args, lead) {
  const sku = args && args.sku ? String(args.sku).trim() : '';
  const qty = args && args.quantity ? Number(args.quantity) : 0;
  const notes = (args && args.notes) ? String(args.notes) : '';

  if (!sku || !qty || qty < 1) return { error: 'sku and quantity required' };

  const product = toolGetProduct(phone, { sku }, lead);
  if (product.error) return product;

  const total = product.price * qty;                       // server computes it, not the model
  const orderId = 'ORD-' + Date.now().toString(36).toUpperCase();

  SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Orders').appendRow([
    orderId, phone, product.sku, product.name, qty,
    product.price, total, product.currency, notes,
    'pending', new Date().toISOString()
  ]);

  return {
    success: true, order_id: orderId, sku: product.sku,
    product_name: product.name, quantity: qty, total: total,
    currency: product.currency, status: 'pending'
  };
}
Enter fullscreen mode Exit fullscreen mode

The glue: expose the tools and dispatch the model's call

This is what turns three functions into a storefront. You register each tool with the schema the model sees, and route whatever it calls to the matching handler. The description on each schema is doing real work — it's how GPT knows not to call log_order until the customer has actually confirmed.

const TOOLS = {
  get_products: {
    handler: toolGetProducts,
    schema: {
      name: 'get_products',
      description: 'List the active, in-stock products this shop sells.',
      parameters: { type: 'object', properties: {} }
    }
  },
  get_product: {
    handler: toolGetProduct,
    schema: {
      name: 'get_product',
      description: 'Look up one product by its SKU.',
      parameters: {
        type: 'object',
        properties: { sku: { type: 'string' } },
        required: ['sku']
      }
    }
  },
  log_order: {
    handler: toolLogOrder,
    schema: {
      name: 'log_order',
      description: 'Log a confirmed order. Only call AFTER the customer confirms the item and quantity.',
      parameters: {
        type: 'object',
        properties: {
          sku: { type: 'string' },
          quantity: { type: 'number' },
          notes: { type: 'string' }
        },
        required: ['sku', 'quantity']
      }
    }
  }
};

// When GPT returns a tool call, run the matching handler and feed the JSON back.
function runToolCall(toolCall, phone, lead) {
  const tool = TOOLS[toolCall.name];
  if (!tool) return { error: 'unknown tool ' + toolCall.name };
  let args = {};
  try { args = JSON.parse(toolCall.arguments || '{}'); }
  catch (e) { return { error: 'bad tool arguments' }; }
  return tool.handler(phone, args, lead);
}
Enter fullscreen mode Exit fullscreen mode

The webhook that receives the WhatsApp message (via Twilio) and the loop that calls GPT with TOOLS are the same pattern I covered in Build a WhatsApp Sales Inbox in Google Sheets — a doPost that returns 200 fast and does the model round-trip on the message text. Drop these tools into that loop and you have a store.

I unit-tested the tool logic before shipping — the cases that matter are a blank stock cell reading as unlimited, an out-of-stock item never reaching the catalog, a case-insensitive SKU, and an order with a missing or zero quantity being rejected instead of written.

The economics

For a shop doing under ~500 messages and ~100 orders a month, the running cost breaks down to roughly $5–10 Twilio, $2–5 OpenAI on GPT-4o-mini, and $0–6 Google — call it under $20/month. A Shopify-plus-app stack for the same thing runs $80–140/month combined. The bigger win is that the orders live in your Drive, not a vendor's database.

When NOT to use this

  • Thousands of SKUs, or a catalog that changes hourly. This shines at 5–30 stable products; past that you want real inventory sync and a database.
  • Variants, tax rules, and international shipping. The model will happily log an order; it won't handle size/color matrices or VAT correctly.
  • Taking payment in-chat. This logs a pending order. Bolt on a Stripe payment link if you need money to move; don't fake a checkout.
  • Letting the model set prices. Note that toolLogOrder re-reads the price from the sheet. Never trust a total the model calculated — recompute it server-side, always.

Wrap-up

An online store, for a lot of small sellers, is three functions: list the catalog, look up an item, log the order — wired to GPT through function calling, backed by two spreadsheet tabs. No website, no monthly platform fee, and the data stays yours.

The production version — Stripe payment links, stock decrement on order, and a merchant dashboard — is written up on the MageSheet blog.

Built by the MageSheet team.

Top comments (0)