WhatsApp chat API

One webhook takes a guest's message and answers with the replies to send, shaped like WhatsApp text, button and list messages. The simulator uses it, and so can curl or a test runner.

Endpoint

POST /api/whatsapp at https://restaurant-test.yomox.app/api/whatsapp, JSON or form-encoded. No sign-in and no CSRF token are needed. Limit: 120 requests a minute per IP address (then 429).

FieldRuleMeaning
fromrequired, +27, 27 or 0 and 9 digitsThe guest's WhatsApp number; bookings and the loyalty card belong to it
message_idrequired, 1-128 of A-Z a-z 0-9 . _ : = -Unique id of this message, used to answer retries safely
textrequired, up to 1000 charactersWhat the guest typed, or the title of the button or row they tapped
reply_idoptional, 1-64 of A-Z a-z 0-9 . _ : -The id of the button or list row tapped
nameoptional, up to 60 charactersThe guest's WhatsApp profile name, used in the greeting

Replies

The answer is JSON: {"to": "+27...", "messages": [...]}, with Cache-Control: no-store. Each message is one of three types. Text uses WhatsApp formatting, such as *bold*.

TypeShapeLimits
text{type, text}Body up to 1024 characters
buttons{type, text, buttons: [{id, title}]}Up to 3 buttons, titles up to 20 characters
list{type, text, button, rows: [{id, title, description}]}Up to 10 rows, titles up to 24 and descriptions up to 72 characters, list button up to 20
curl -s -X POST https://restaurant-test.yomox.app/api/whatsapp -H 'Content-Type: application/json' \
  -d '{"from":"+27820000000","message_id":"demo-1","text":"hi","name":"Thandi"}'

{"to":"+27820000000","messages":[{"type":"list",
  "text":"Hello Thandi, welcome to *Fynbos Kitchen* in Stellenbosch.\n\nWhat would you like to do?",
  "button":"Choose","rows":[
    {"id":"food","title":"See our menu","description":"Starters, mains, desserts and drinks"},
    {"id":"book","title":"Book a table","description":"Lunch or dinner, Tuesday to Sunday"},
    {"id":"bookings","title":"My bookings","description":"See or cancel a booking"},
    {"id":"loyalty","title":"Loyalty card","description":"Collect stamps for a free main course"},
    {"id":"info","title":"Hours and location","description":"Find us in Stellenbosch"}]}]}
# Tap "Loyalty card" in that list
curl -s -X POST https://restaurant-test.yomox.app/api/whatsapp -H 'Content-Type: application/json' \
  -d '{"from":"+27820000000","message_id":"demo-2","text":"Loyalty card","reply_id":"loyalty"}'

{"to":"+27820000000","messages":[{"type":"buttons",
  "text":"*Fynbos Kitchen loyalty card*\n\nEvery booking you make earns a stamp. ...",
  "buttons":[{"id":"join","title":"Join loyalty card"},{"id":"main","title":"Main menu"}]}]}

How input is read

  • A reply_id wins. Without one, the text is matched against the current options by number (2) or by title (book a table, any case).
  • hi, hello, menu, start or restart go back to the main menu from anywhere. help or ? explains how to reply.
  • Anything else answers "Sorry, I didn't understand that." and shows the same options again.
  • After 30 minutes without a message, the next message starts again at the main menu.

Retries

WhatsApp can deliver the same message twice. The same message_id from the same number gets the stored answer again, and nothing is booked twice. The same message_id from another number gets 409 "That message_id has already been used." A request that breaks a rule gets 422 with a JSON list of errors.

Menu map

  1. See our menu: starters, mains, desserts and drinks with prices.
  2. Book a table: a day in the next week (closed Mondays), then a seating with seats left, then the party size (1 to 12; bigger groups are asked to phone), then Confirm. The answer gives a reference such as FBK-7KQ2XD.
  3. My bookings: upcoming bookings for this number. Pick one to see it, then cancel it or keep it.
  4. Loyalty card: join, see your stamps, and redeem a free main course.
  5. Hours and location: address, phone number and opening times.

Seatings

TimeSeats
12:3030
18:0040
19:3040
21:0012

Seats are taken inside one database transaction, so two guests can never book the last seats twice. Cancelling a booking puts its seats back straight away.

Loyalty card rules

  • Joining is free. The loyalty card number (LC-xxxxxx) is linked to the WhatsApp number.
  • Every confirmed booking earns 1 stamp. Joining straight after a booking stamps that booking too.
  • Cancelling a booking takes its stamp off again.
  • 8 stamps can be redeemed for a voucher (FREE-xxxxxx) for any main course, valid for 30 days. Redeeming uses 8 stamps.

Meals are paid at the restaurant. The chat never asks for credit card details.

Test data

  • +27 82 000 0808 already has loyalty card LC-DEMO08 with 8 stamps, so you can try redeeming a free main.
  • The 21:00 seating has only 12 seats, so you can try asking for more than are left.
  • Any other valid South African number starts with no bookings and no loyalty card.