Skip to content

Latest commit

 

History

12 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

WhatsApp Flow Bot (MVP)

A very small FastAPI service that answers WhatsApp messages through GREEN-API with predefined flows only — no AI, no database, no background workers.

What it does:

  1. A new customer sends any text → the bot replies with a bilingual language menu (two tappable buttons: עברית / English).
  2. The customer picks a language → the bot sends the interest menu in that language (three tappable buttons: Pilates / Barre / instructor course).
  3. The customer picks an interest → the bot sends that flow's description plus its own three follow-up buttons (pricing, trial class, talk to us …).
  4. The customer picks a follow-up → the bot sends the answer, or hands the chat to a human if they asked to talk to someone.
  5. Typing 0 at any point goes back to the main menu.
  6. The moment the business owner replies manually from WhatsApp, the bot goes silent for that chat.

Requirements

  • Python 3.11+
  • A GREEN-API instance (instance id + API token)

Local development

python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env      # then fill in your GREEN-API credentials
uvicorn app.main:app --reload

Then:

Run the tests (GREEN-API is mocked, no real WhatsApp messages are sent):

pytest

Environment variables

Variable Secret Description
GREEN_API_INSTANCE_ID yes Your GREEN-API instance number.
GREEN_API_TOKEN yes Your GREEN-API apiTokenInstance.
GREEN_API_API_URL yes API host, e.g. https://api.green-api.com or your instance host https://7105.api.greenapi.com.
ADMIN_API_KEY yes Any long random string. Required by the /admin/* endpoints.
DATA_FILE_PATH no Where customer state is written. data/users.json on Render Free.
GREEN_API_MEDIA_URL no Host that files are uploaded to. Blank derives it from the API URL.
ASSETS_DIR no Where images the bot sends live. assets by default.
LOG_LEVEL no INFO by default. DEBUG also logs every webhook body and GREEN-API response.

Never commit .env. A blank value (GREEN_API_API_URL=) is treated as "not set" and falls back to the default, and a host pasted without a scheme gets https:// added.

Endpoints

Method Path Purpose
GET /health Render health check. Returns {"status": "ok"}. Never contacts GREEN-API.
POST /webhook/green-api GREEN-API notifications. Always returns HTTP 200.
GET /admin/diagnostics Start here when something is wrong. Checks the whole setup.
GET /admin/user/{chat_id} Read a customer's stored state.
POST /admin/pause/{chat_id} Stop automatic replies for a customer.
POST /admin/resume/{chat_id} Re-enable automatic replies.
POST /admin/reset/{chat_id} Forget a customer so onboarding starts again.

Admin endpoints require the header X-Admin-Key: <ADMIN_API_KEY>:

curl -H "X-Admin-Key: $ADMIN_API_KEY" https://<service-name>.onrender.com/admin/user/972501234567@c.us

Human takeover

GREEN-API distinguishes the two kinds of outgoing message, and the bot relies on that:

  • outgoingMessageReceived — the owner typed the message on their phone / WhatsApp Web / Desktop. The bot immediately sets bot_enabled: false and flow: "human" for that chat and stops replying.
  • outgoingAPIMessageReceived — the message was sent by this application through GREEN-API. The bot is not disabled.

To bring the bot back for that customer, call POST /admin/resume/{chat_id} (keeps the conversation state) or POST /admin/reset/{chat_id} (starts onboarding over).

Storage is temporary on Render Free

Render Free Web Services have an ephemeral filesystem. data/users.json is wiped whenever the service spins down, restarts, or redeploys. When that happens a returning customer looks like a new inquiry and gets the language menu again.

This is intentional for the MVP. There is deliberately no Git/GitHub/database workaround.

All state access goes through app/storage/json_store.py, so upgrading later is a configuration change, not a rewrite:

  1. Upgrade the Render instance to a paid plan.
  2. Attach a Persistent Disk mounted at /var/data.
  3. Set DATA_FILE_PATH=/var/data/users.json.

No bot or flow code changes.

When something is not working

1. Ask the service what is wrong. This one endpoint checks the configuration, the GREEN-API instance state and the instance's webhook settings, and answers in plain words:

curl -H "X-Admin-Key: $ADMIN_API_KEY" https://<service-name>.onrender.com/admin/diagnostics
{
  "status": "problems_found",
  "problems": [
    "GREEN-API instance state is 'notAuthorized', expected 'authorized'. Scan the QR code again in the GREEN-API console.",
    "The instance webhookUrl is 'https://old-bot.example.com/hook', which does not end in /webhook/green-api - notifications are going somewhere else.",
    "GREEN-API setting outgoingMessageWebhook is 'no', expected 'yes' - needed to detect a manual reply and switch the bot off."
  ]
}

"status": "ok" with an empty problems list means the setup is fine and the issue is in the conversation itself — read the logs.

2. Read the logs. On Render: your service → Logs. Every startup prints the resolved configuration (never the secrets themselves):

INFO app.main: WhatsApp flow bot starting
INFO app.main:   GREEN_API_API_URL     = https://api.green-api.com
INFO app.main:   GREEN_API_INSTANCE_ID = 1101234567
INFO app.main:   GREEN_API_TOKEN       = set
ERROR app.main: CONFIG PROBLEM: ADMIN_API_KEY is not set - the /admin endpoints will answer 503.

and every message is traceable end to end:

INFO app.routers.webhook: Webhook received: type='incomingMessageReceived' chatId='972501112222@c.us' idMessage='F2' typeMessage='interactiveButtonsReply'
INFO app.services.bot: Handling '1' from 972501112222@c.us (step=1, language=None, flow=None)
INFO app.services.bot: 972501112222@c.us chose language he
INFO app.services.green_api: Sending 3 buttons to 972501112222@c.us: ['Pilates מכשירים', 'Barre', 'קורס הכשרת מדריכות']
INFO app.services.green_api: GREEN-API -> POST https://api.green-api.com/waInstance1101234567/sendInteractiveButtons/***TOKEN***
INFO app.services.green_api: Sent button menu BAE5A7BA6D6E28EB to 972501112222@c.us

The API token is masked as ***TOKEN*** everywhere, including inside error messages. Set LOG_LEVEL=DEBUG to also log every webhook body and every GREEN-API response body.

3. Common causes, and the log line that shows each one.

Symptom in the log Cause
CONFIG PROBLEM: GREEN_API_TOKEN is not set The env var is missing on Render.
GREEN_API_API_URL looks wrong The URL has no http:// or https://.
returned HTTP 401 GREEN_API_TOKEN does not match the instance.
returned HTTP 404 GREEN_API_INSTANCE_ID or GREEN_API_API_URL is wrong.
returned HTTP 466 The GREEN-API instance is out of quota.
Bot is OFF for … - ignoring the message A manual reply switched the bot off. POST /admin/resume/{chat_id} turns it back on.
Ignoring imageMessage from … The customer sent media; the bot only reads text and button taps.
Duplicate notification … - already answered GREEN-API redelivered a notification. Working as intended.
Nothing at all in the log The webhook never arrived — check webhookUrl and incomingWebhook in /admin/diagnostics.

Every webhook still answers HTTP 200 (so GREEN-API stops retrying), but a failed one returns {"status": "error", "reason": "..."} naming the cause.

Menus are interactive buttons

Both menus are sent with GREEN-API's sendInteractiveButtons. GREEN-API limits a message to 3 buttons, each with a buttonText of at most 25 characters; app/services/green_api.py enforces both before sending.

GREEN-API marks that method as beta, so the bot never depends on it:

  • If the button call fails, the same menu is re-sent as a numbered text message (1. … / 2. … / 3. …) built from the very same wording — nothing to maintain twice.
  • A customer is understood whether they tap a button, type the number, or type the button's label ("English", "barre").

A tap arrives as its own notification type, not as text, and GREEN-API has several names for one — including interactiveButtonsResponse, which production instances send but no published documentation page describes. So the bot does not match on typeMessage: it scans every nested object of messageData for a selection key (selectedButtonId, selectedId, selectedRowId, buttonId, or their text equivalents). That covers the documented shapes —

typeMessage Where the choice is
buttonsResponseMessage selectedButtonId / selectedButtonText
templateButtonsReplyMessage selectedId / selectedDisplayText
listResponseMessage singleSelectReply.selectedRowId
interactiveButtonsResponse undocumented; found by the same scan

— and a new one GREEN-API invents next will very likely work unchanged.

Lists are never scanned, so a menu echoed back to us (interactiveButtons / interactiveButtonsReply, which carry every button and select none) can never be mistaken for a choice. Anything the bot still cannot read logs Could not read a choice out of ... with the complete messageData — the one thing needed to add support for that shape.

Sending images

assets/class_schedule.jpeg is sent automatically after the answer to any schedule button, via GREEN-API's sendFileByUpload. The file is uploaded from disk, so nothing depends on the repository being public. Note this goes to the media host, not the API host — see GREEN_API_MEDIA_URL above.

Which button sends which image is data, in FLOW_ANSWER_IMAGES in each catalogue:

FLOW_ANSWER_IMAGES = {
    "pilates": {"2": "class_schedule.jpeg"},   # מערכת שעות
    "barre": {"1": "class_schedule.jpeg"},     # מחירים ומערכת שעות
}

One sheet covers both Pilates and Barre, so all four schedule buttons send it: Hebrew Pilates 2, Hebrew Barre 1, and English Pilates and Barre 1. To attach an image to another button, drop the file in assets/ and add a line here. To replace the schedule, overwrite assets/class_schedule.jpeg — the filename is what the code looks up.

The image is sent after the text answer, and a failure to send it is logged rather than raised, so a missing file never swallows the answer that already went out. Startup and /admin/diagnostics both report any configured image that is not on disk, and a test asserts every configured file exists.

Going back: 0

Typing 0 at any step returns the customer to the main (interest) menu, clearing the flow they were in. It works before they have finished — and after, when the bot would otherwise stay quiet. Before a language has been picked there is no main menu yet, so 0 re-asks the language question instead.

0 is not a button: GREEN-API allows only three per message and all three are in use. It is advertised in each menu's footer (MENU_FOOTER in the catalogues) and is matched whether typed or arriving as a tapped button id.

One deliberate exception: 0 does not wake the bot in a chat a human has taken over. Only POST /admin/resume/{chat_id} does that.

Editing the wording

All customer-facing text lives in app/messages/he.py and app/messages/en.py:

Name What it is
LANGUAGE_BODY / LANGUAGE_BUTTONS The opening bilingual menu.
INTEREST_BODY / INTEREST_BUTTONS The three interest buttons.
FLOW_BODY[flow] The description sent when that interest is chosen.
FLOW_BUTTONS[flow] That flow's own follow-up buttons.
PRICING The class price list. Both Pilates and Barre point at it, so a price is one edit.
FLOW_ANSWERS[flow][buttonId] What the bot answers for a follow-up — a list of 1–3 messages. An empty list means the image alone is the answer.
HANDOVER_BUTTONS[flow] Follow-up buttons that fetch a human instead of answering.
HANDOVER_MESSAGE Sent just before a human takes over.
MENU_FOOTER The "0 goes back" hint under the interest and flow menus.

Everything still marked [TODO: ...] needs the real wording — those are the answers behind each follow-up button, and the handover message.

Each button's buttonId is what routes the customer: on the interest menu 1 → pilates, 2 → barre, 3 → instructor_course (see FLOW_BY_BUTTON_ID in app/services/bot.py). Change a buttonText freely; change a buttonId only together with the matching FLOW_ANSWERS / HANDOVER_BUTTONS entry. Keep labels within 25 characters and each menu at three buttons or fewer — a test enforces both, and that every button leads somewhere.

The two catalogues are deliberately allowed to differ: the Hebrew Pilates menu offers מחירים ומנויים / מערכת שעות / לקבוע שיעור ניסיון, while the English one offers Pricing & class schedule / Book a trial class / Talk to us. Only the English Pilates menu has a "talk to us" button.

Deploy to Render (Free)

  1. Push this repository to GitHub.

  2. Open the Render Dashboard.

  3. New → Blueprint, connect this repository. Render reads render.yaml.

  4. Confirm the instance type is Free (plan: free is already set in render.yaml).

  5. Render prompts for the values marked sync: false — enter your GREEN_API_INSTANCE_ID, GREEN_API_TOKEN, GREEN_API_API_URL and ADMIN_API_KEY.

  6. Apply / Deploy.

  7. Verify the service is up:

    https://<service-name>.onrender.com/health
    

    It must return {"status": "ok"}.

  8. In the GREEN-API console, set the instance Webhook URL to:

    https://<service-name>.onrender.com/webhook/green-api
    
  9. Enable these instance settings in GREEN-API:

    • incomingWebhook — yes (receive customer messages)
    • outgoingMessageWebhook — yes (required for human takeover detection)
    • outgoingAPIMessageWebhook — yes
    • stateWebhook — optional (ignored by the bot)

Render binds the port it gives you through $PORT; the start command in render.yaml is uvicorn app.main:app --host 0.0.0.0 --port $PORT. Port 8000 is never hardcoded for production.

Free services sleep after inactivity, so the first webhook after a sleep may take a few seconds to be answered while the service wakes up.

Project structure

app/
    main.py              FastAPI app, /health, router wiring
    config.py            Pydantic Settings
    routers/
        webhook.py       POST /webhook/green-api
        admin.py         /admin/* endpoints (X-Admin-Key)
    services/
        green_api.py     All GREEN-API HTTP calls (httpx)
        bot.py           Flow rules, human takeover
    storage/
        json_store.py    JSON state, atomic writes, duplicate detection
    messages/
        he.py / en.py    All customer-facing text
data/                    Temporary state (users.json, gitignored)
tests/test_bot.py        Flow tests with GREEN-API mocked
render.yaml              Render Blueprint (Free plan, no disk, no database)

About

WhatsApp flow bot for a Pilates studio: Hebrew/English button menus via GREEN-API, built with FastAPI.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages