Tobi is a WhatsApp-first print-ordering demo for Indian print shops. Customers send a PDF and print instructions in WhatsApp, Tobi uses a hybrid AI understanding layer to interpret the message, asks for missing details, prepares a deterministic quote, sends a Razorpay Test Mode payment link, and updates a shop dashboard when payment is confirmed.
The app is built as a Hono app on Cloudflare Workers with Cloudflare D1, R2, KV, WhatsApp Cloud API webhooks, Gemini message understanding, and Razorpay Test Mode webhooks. Twilio sandbox parsing remains only as a legacy fallback for form-encoded smoke tests.
tobi-demo.mp4
- A customer messages the WhatsApp Cloud API business number.
- Tobi receives the inbound webhook at
/webhooks/whatsapp. - PDF files are stored in R2 and the PDF page count is treated as authoritative.
- Gemini returns structured message understanding: intent, confidence, normalized print slots, ambiguity, and optional general-chat reply.
- Backend code validates the understanding and performs all order, file, quote, payment, and state changes.
- Tobi asks for missing information one field at a time.
- Tobi shows a confirmation summary with pages, copies, layout, sides, pickup time, billable sheets, and total price.
- Before payment starts, customers can change details such as "make it color instead" and Tobi recomputes the quote.
- The customer taps
ConfirmorCancelwhen WhatsApp interactive buttons are available, or sends the same words as text. - On confirmation, Tobi creates a Razorpay Test Mode payment link.
- Razorpay posts payment events to
/webhooks/razorpay. - Paid orders appear in the dashboard for shop processing.
- The shop updates order status from the dashboard.
Current Worker URL:
https://tobi.rithvik-padma.workers.dev
Production WhatsApp test chat:
Important routes:
GET /health
POST /webhooks/whatsapp
POST /webhooks/razorpay
GET /dashboard/login
GET /dashboard/orders
GET /dashboard/orders/:id
POST /dashboard/orders/:id/status
The dashboard is deployed in the same Worker as the webhooks. It reads from the same remote D1 database that WhatsApp intake and Razorpay payment confirmation write to.
The dashboard is a shop console for reviewing and processing orders. It includes:
- Order list with customer WhatsApp contact.
- Payment status and order status.
- File count and PDF download.
- Print options: page count, copies, color, sides, layout, paper, binding, pickup time.
- Quote snapshot and total amount.
- Status controls for shop workflow.
Login is protected by ADMIN_PIN and an HTTP-only dashboard session cookie.
WhatsApp Cloud API
-> WhatsApp webhook
-> Hono Worker
-> Gemini message understanding
-> deterministic backend workflow
-> D1 orders/messages/payments
-> R2 PDF storage
-> Razorpay payment link
-> Razorpay webhook
-> D1 payment/order update
-> Dashboard
The AI boundary is intentionally narrow: Gemini interprets print-domain WhatsApp messages, while backend code validates and executes the workflow. Pricing, payment links, PDF storage, PDF page counts, missing-field prompts, and order state transitions are deterministic.
Examples:
two copiesupdates the active order copy count when a PDF/order is already in progress.make it color insteadupdates a pre-payment quote and returns a refreshed confirmation summary.how much now?returns a quote when enough details are present, or asks for the next missing detail.
Core storage:
- D1: customers, orders, messages, payments, webhook events, order events, pricing rules.
- R2: uploaded PDF files.
- KV: dashboard/session support.
- Worker secrets: Meta WhatsApp, Razorpay, Gemini, dashboard credentials, and optional Twilio fallback credentials.
Install dependencies:
bun installCreate local environment variables:
cp .dev.vars.example .dev.varsFill .dev.vars with local/demo credentials. Do not commit .dev.vars.
Apply local D1 migrations:
bunx wrangler d1 migrations apply tobi-demo-db --localStart the local Worker:
bun run devOpen:
http://localhost:8787/dashboard/login
Required for the complete WhatsApp Cloud API, Gemini, Razorpay, and dashboard workflow:
APP_ENV
PUBLIC_APP_URL
DEFAULT_CURRENCY
DEMO_SHOP_ID
DEMO_SHOP_NAME
ADMIN_PIN
ADMIN_SESSION_TOKEN
WHATSAPP_ACCESS_TOKEN
WHATSAPP_APP_SECRET
WHATSAPP_VERIFY_TOKEN
WHATSAPP_PHONE_NUMBER_ID
WHATSAPP_BUSINESS_ACCOUNT_ID
WHATSAPP_GRAPH_API_VERSION
RAZORPAY_KEY_ID
RAZORPAY_KEY_SECRET
RAZORPAY_WEBHOOK_SECRET
GEMINI_API_KEY
GEMINI_DEFAULT_MODEL
Optional for the legacy Twilio sandbox fallback:
TWILIO_ACCOUNT_SID
TWILIO_AUTH_TOKEN
TWILIO_WHATSAPP_FROM
For local testing without external services, some tests use mocks and fixtures. For real WhatsApp and payment testing, WhatsApp Cloud API, Razorpay, and Gemini values are needed. Twilio values are optional and only support the legacy sandbox/form-encoded path.
Set the WhatsApp callback URL to:
https://<worker-url>/webhooks/whatsapp
Use this verify token:
WHATSAPP_VERIFY_TOKEN
Subscribe the webhook to WhatsApp message events. Inbound JSON webhooks are handled as WhatsApp Cloud API messages, and outbound replies are sent through the Graph API using:
WHATSAPP_ACCESS_TOKEN
WHATSAPP_PHONE_NUMBER_ID
WHATSAPP_GRAPH_API_VERSION
Set WHATSAPP_APP_SECRET to verify x-hub-signature-256 webhook signatures.
Twilio sandbox support is kept in src/services/twilio.ts for fallback smoke tests and older demos. If you use it, set the inbound WhatsApp webhook to:
https://<worker-url>/webhooks/whatsapp
Use method:
POST
The public URL must match PUBLIC_APP_URL because Twilio signature verification depends on the exact webhook URL.
Use Razorpay Test Mode keys for:
RAZORPAY_KEY_ID
RAZORPAY_KEY_SECRET
Create a webhook for:
https://<worker-url>/webhooks/razorpay
Set the webhook secret to the same value as:
RAZORPAY_WEBHOOK_SECRET
Enable these events:
payment.captured
payment_link.paid
payment_link.expired
payment_link.cancelled
The Worker needs configured bindings for:
- D1 database:
DB - R2 bucket:
FILES - KV namespace:
SESSIONS - Queue producer:
JOB_QUEUE
Secrets should be uploaded with Wrangler:
bunx wrangler secret put ADMIN_PIN
bunx wrangler secret put ADMIN_SESSION_TOKEN
bunx wrangler secret put WHATSAPP_ACCESS_TOKEN
bunx wrangler secret put WHATSAPP_APP_SECRET
bunx wrangler secret put WHATSAPP_VERIFY_TOKEN
bunx wrangler secret put RAZORPAY_KEY_ID
bunx wrangler secret put RAZORPAY_KEY_SECRET
bunx wrangler secret put RAZORPAY_WEBHOOK_SECRET
bunx wrangler secret put GEMINI_API_KEYOptional legacy Twilio fallback secrets:
bunx wrangler secret put TWILIO_ACCOUNT_SID
bunx wrangler secret put TWILIO_AUTH_TOKEN
bunx wrangler secret put TWILIO_WHATSAPP_FROMPricing is deterministic and handled in src/services/pricing.ts.
Important rules:
- PDF page count is authoritative.
- N-up layout changes billable sheets, not the original PDF page count.
- Double-sided printing reduces billable sheets after layout is applied.
- Staple binding is the free default when the customer does not mention binding.
- Generic binding requests, such as "I want binding", are treated as spiral binding.
- Spiral binding is charged per copy.
- A small demo platform fee is added.
Example: a 5-page PDF, 2 copies, black and white, double-sided, spiral binding:
Printing: 6 billable sheets
Binding: spiral x 2 copies
Platform fee: demo fee
Run the test suite:
bun testRun the typecheck:
bun run lintRun the combined verification command:
bun run verifyThe tests cover:
- WhatsApp message handling.
- General conversation replies.
- Hybrid AI message understanding.
- Adaptive print-domain follow-up messages.
- Order state transitions.
- PDF page-count handling.
- N-up layout extraction.
- Quote calculation.
- Duplicate inbound message handling.
- WhatsApp Cloud API webhook handling and outbound Graph API replies.
- Legacy Twilio signature verification.
- Razorpay webhook idempotency.
- Dashboard rendering.
Recommended next improvements based on the current customer and shop flows:
- Show the active order state in every customer reply once an order exists, especially while waiting for file/details/payment.
- Prefer WhatsApp interactive buttons for quote confirmation, cancellation, and payment-help choices, with text fallbacks for unsupported clients.
- Add dashboard filters for paid, ready-for-pickup, and stuck orders so shop staff can scan the queue faster.
- Add a clearer payment-progress message after sending the Razorpay link, including what the customer should do if payment succeeds but the order does not update.
- Surface file metadata in customer replies after PDF upload, including filename and page count, before asking for missing print options.
- This is a demo, not a production payment or fulfillment system.
- Payments use Razorpay Test Mode.
- WhatsApp is currently integrated through the WhatsApp Cloud API. Twilio sandbox support is retained only as a fallback test path.
- The dashboard is PIN-protected, not a full multi-user auth system.
- V1 is optimized for one demo shop and pickup-only orders.
- Human review is still recommended before printing real customer documents.
src/
app.ts Hono route wiring, WhatsApp flow, dashboard HTML
domain.ts Zod schemas and domain types
index.ts Worker entrypoint
store.ts D1 and in-memory stores
db/migrations/ D1 migrations
services/ WhatsApp, extraction, pricing, payments, PDF intake, notifications
utils/ Shared formatting and ID helpers
tests/ Vitest coverage
assets/ Product and README assets
wrangler.toml Cloudflare Worker bindings and deploy config
bun install
bun run dev
bun test
bun run lint
bun run verify
bunx wrangler deploy- Never commit
.dev.vars,.env, API keys, webhook secrets, or dashboard secrets. - Keep Razorpay webhook secret and Worker secret in sync.
- Keep Meta webhook verify token and app secret in sync with the Worker secrets.
- Keep Twilio webhook URL and
PUBLIC_APP_URLin sync only when using the legacy sandbox fallback. - Use Test Mode credentials for demos.