/
Documentation

Tutorial: WhatsApp Invoice Reminder

Build a complete add-on that lets users send invoice payment reminders via WhatsApp with one click. Perfect first add-on project.

What you'll learn

• Setting up a project from scratch
• Using the SDK provider and hooks
• Fetching invoices with filters
• Showing loading and empty states
• Using navigation and notifications
• Deploying and registering on the marketplace

Final result:

An add-on that shows unpaid invoices and opens WhatsApp with a pre-filled payment reminder message when clicked.

Difficulty: BeginnerTime: 20 minutesScopes: invoices.read, customers.read

Step 1: Create the Project

Terminal
# Create a new React + TypeScript project
npm create vite@latest whatsapp-reminder -- --template react-ts
cd whatsapp-reminder

# Install the SyncBooks SDK
npm install @syncbooks/addon-sdk

# Install Tailwind (optional but recommended)
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

Configure Tailwind

tailwind.config.js
export default {
  content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
  theme: { extend: {} },
  plugins: [],
}
src/index.css
@tailwind base;
@tailwind components;
@tailwind utilities;

Step 2: Set Up the Entry Point

src/main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { SyncBooksProvider } from '@syncbooks/addon-sdk'
import App from './App'
import './index.css'

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <SyncBooksProvider>
      <App />
    </SyncBooksProvider>
  </React.StrictMode>
)

Why SyncBooksProvider?

This component establishes the connection to SyncBooks, receives authentication tokens, and makes all SDK hooks work. Without it, nothing functions.

Step 3: Build the App Component

This is the main component. It shows unpaid invoices and lets users send WhatsApp reminders:

src/App.tsx
import { useState } from 'react'
import { useSyncBooks, useInvoices, ui } from '@syncbooks/addon-sdk'

export default function App() {
  const { ready, scopes } = useSyncBooks()

  // ─── Loading State ─────────────────────────────────────────────
  if (!ready) {
    return (
      <div className="flex items-center justify-center min-h-screen bg-white">
        <div className="text-center">
          <div className="h-8 w-8 mx-auto animate-spin rounded-full border-2 border-green-600 border-t-transparent" />
          <p className="text-sm text-gray-500 mt-3">Connecting to SyncBooks...</p>
        </div>
      </div>
    )
  }

  // ─── Permission Check ──────────────────────────────────────────
  const hasAccess = scopes.includes('invoices.read') || scopes.includes('invoices.*') || scopes.includes('*')
  if (!hasAccess) {
    return (
      <div className="p-8 text-center">
        <p className="text-red-600 font-semibold">⚠️ Permission Required</p>
        <p className="text-sm text-gray-500 mt-2">
          This add-on needs <code className="bg-gray-100 px-1 rounded">invoices.read</code> permission.
          Ask your admin to reinstall with this scope.
        </p>
      </div>
    )
  }

  return <InvoiceList />
}

// ─── Invoice List Component ────────────────────────────────────────
function InvoiceList() {
  // Fetch unpaid invoices (status = 'sent' means awaiting payment)
  const {
    data: invoices,
    loading,
    error,
    total,
    page,
    totalPages,
    nextPage,
    prevPage,
  } = useInvoices({ status: 'sent', limit: 10 })

  // Track which invoices we've sent reminders for
  const [sentIds, setSentIds] = useState<Set<string>>(new Set())

  // ─── Send WhatsApp Reminder ──────────────────────────────────
  function sendReminder(invoice: any) {
    const customer = invoice.customerId
    const name = customer?.name || 'Customer'
    const phone = customer?.phone?.replace(/[\s-]/g, '') || ''
    const amount = invoice.totalAmount?.toFixed(2) || '0.00'
    const invoiceNum = invoice.invoiceNumber || 'your invoice'
    const dueDate = invoice.dueDate 
      ? new Date(invoice.dueDate).toLocaleDateString() 
      : 'soon'

    // Build WhatsApp message
    const message = encodeURIComponent(
      `Hi ${name},\n\n` +
      `This is a friendly reminder that ${invoiceNum} for GHS ${amount} ` +
      `is due by ${dueDate}.\n\n` +
      `Please make payment at your earliest convenience.\n\n` +
      `Thank you! 🙏`
    )

    // Open WhatsApp (with phone if available, without if not)
    const url = phone
      ? `https://wa.me/${phone}?text=${message}`
      : `https://wa.me/?text=${message}`

    window.open(url, '_blank')

    // Mark as sent
    setSentIds(prev => new Set([...prev, invoice._id]))

    // Show toast in SyncBooks
    ui.notify(`WhatsApp opened for ${invoiceNum}`, 'success')
  }

  // ─── Loading ─────────────────────────────────────────────────
  if (loading) {
    return (
      <div className="p-6">
        <Header total={0} />
        <div className="space-y-3 mt-4">
          {[1, 2, 3, 4, 5].map(i => (
            <div key={i} className="h-16 bg-gray-100 rounded-lg animate-pulse" />
          ))}
        </div>
      </div>
    )
  }

  // ─── Error ───────────────────────────────────────────────────
  if (error) {
    return (
      <div className="p-6">
        <Header total={0} />
        <div className="mt-4 p-4 bg-red-50 border border-red-200 rounded-lg">
          <p className="text-sm text-red-700">Error loading invoices: {error}</p>
          <button 
            onClick={() => window.location.reload()}
            className="mt-2 text-xs text-red-600 hover:underline"
          >
            Try again
          </button>
        </div>
      </div>
    )
  }

  // ─── Render ──────────────────────────────────────────────────
  return (
    <div className="p-4 sm:p-6 max-w-2xl mx-auto">
      <Header total={total} />

      {/* Empty State */}
      {invoices.length === 0 ? (
        <div className="text-center py-16">
          <p className="text-4xl mb-3">🎉</p>
          <p className="font-semibold text-gray-700">All invoices paid!</p>
          <p className="text-sm text-gray-500 mt-1">No pending invoices to remind about.</p>
        </div>
      ) : (
        <>
          {/* Invoice Cards */}
          <div className="space-y-3 mt-4">
            {invoices.map((invoice: any) => (
              <div
                key={invoice._id}
                className={`flex items-center justify-between p-4 rounded-lg border transition-colors ${
                  sentIds.has(invoice._id)
                    ? 'bg-green-50 border-green-200'
                    : 'bg-white border-gray-200 hover:border-gray-300'
                }`}
              >
                {/* Invoice Info */}
                <div className="min-w-0 flex-1">
                  <div className="flex items-center gap-2">
                    <span className="font-medium text-sm text-gray-900">
                      {invoice.invoiceNumber}
                    </span>
                    {sentIds.has(invoice._id) && (
                      <span className="text-[10px] bg-green-100 text-green-700 px-1.5 py-0.5 rounded-full font-medium">
                        ✓ Sent
                      </span>
                    )}
                  </div>
                  <p className="text-xs text-gray-500 mt-0.5">
                    {invoice.customerId?.name || 'Unknown'} — GHS {invoice.totalAmount?.toFixed(2)}
                  </p>
                  <p className="text-xs text-gray-400">
                    Due: {invoice.dueDate ? new Date(invoice.dueDate).toLocaleDateString() : 'N/A'}
                  </p>
                </div>

                {/* Send Button */}
                <button
                  onClick={() => sendReminder(invoice)}
                  className="shrink-0 ml-3 flex items-center gap-1.5 px-3 py-2 bg-green-600 text-white text-xs font-medium rounded-lg hover:bg-green-700 transition-colors"
                >
                  {/* WhatsApp Icon */}
                  <svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
                    <path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z"/>
                  </svg>
                  Send
                </button>
              </div>
            ))}
          </div>

          {/* Pagination */}
          {totalPages > 1 && (
            <div className="flex items-center justify-between mt-6 pt-4 border-t">
              <button
                onClick={prevPage}
                disabled={page <= 1}
                className="text-xs px-3 py-1.5 border rounded-md disabled:opacity-40"
              >
                ← Previous
              </button>
              <span className="text-xs text-gray-500">
                Page {page} of {totalPages}
              </span>
              <button
                onClick={nextPage}
                disabled={page >= totalPages}
                className="text-xs px-3 py-1.5 border rounded-md disabled:opacity-40"
              >
                Next →
              </button>
            </div>
          )}
        </>
      )}
    </div>
  )
}

// ─── Header Component ────────────────────────────────────────────
function Header({ total }: { total: number }) {
  return (
    <div className="flex items-center justify-between">
      <div>
        <h1 className="text-lg font-bold text-gray-900 flex items-center gap-2">
          📱 WhatsApp Reminders
        </h1>
        <p className="text-xs text-gray-500">Send payment reminders via WhatsApp</p>
      </div>
      {total > 0 && (
        <span className="text-xs bg-amber-100 text-amber-700 px-2.5 py-1 rounded-full font-medium">
          {total} unpaid
        </span>
      )}
    </div>
  )
}

Step 4: Run Locally

Terminal
npm run dev
# Open http://localhost:5173

You'll see the loading state. API calls won't work until you deploy and register the add-on (or configure a test token).

Step 5: Deploy to Vercel

Terminal
npm run build
npx vercel deploy --prod

# Output: https://whatsapp-reminder-abc123.vercel.app

Step 6: Register on the Marketplace

1

Go to /developers → Marketplace Add-ons → New Addon

Fill in:

2

Set manifest fields

  • Name: WhatsApp Invoice Reminder
  • Short Description: Send payment reminders to customers via WhatsApp
  • Launch URL: https://whatsapp-reminder-abc123.vercel.app
  • Category: Communication
  • Permissions: invoices.read, customers.read
  • Placements: navigation.item, invoice.actions
  • Pricing: Free
3

Create and test

Click Create. Go to your dashboard → Add-ons → open your addon. It should show your invoices with WhatsApp send buttons!

4

Submit for review

When everything works, click Submit. A SyncBooks admin will review and publish it.

How It Works

  • useSyncBooks() — checks connection status and granted scopes
  • useInvoices({ status: 'sent' }) — fetches unpaid invoices with auto-pagination
  • ui.notify() — shows a toast notification inside SyncBooks
  • window.open() — opens WhatsApp with a pre-filled message
  • Local sentIds state — tracks which invoices have been reminded

Next Tutorial

Ready for something more advanced? Build an Expense Receipt Scanner with file upload, OCR, and its own backend.