Open-source project
fbsamples/whatsapp-business-jaspers-market avatar
fbsamples/whatsapp-business-jaspers-market

whatsapp-business-jaspers-market: a reference WhatsApp bot with real attribution

Sample Whatsapp App - Jasper's Market

715 stars566 forksJavaScriptApache-2.0

At a glance

What is it?
Jasper's Market is Meta's sample WhatsApp Business app for a fictional grocery brand, showing the platform end to end from templates and media uploads to a CloudWatch attribution event. It is small, in plain JavaScript on Express, and the most interesting part is the measurement design: a single JSON event on stdout, deduplicated by inbound message id, with no Redis on the message path.
Who is it for?
Adopt this sample if you are building a WhatsApp Business integration and want a working reference for the whole path, from the Meta app and business account setup through template creation, runtime media upload, the tunnel, the Express webhook and the CloudWatch attribution event.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 29 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A fictional grocery brand that exists to exercise the platform

The framing is stated at the top of the readme. Jasper's Market is a fictional grocery brand created to demonstrate key features of the WhatsApp Business Platform, and the bot uses those features to deliver what the documentation calls a great customer experience. The intended use is inspiration, since using this demo as inspiration you can create a WhatsApp experience that combines both automation and live customer support.

That is a different goal from a starter kit, and it changes how you should read the code. A starter kit gives you abstractions to build on, a sample gives you one complete worked example, and the sample's value is in showing the exact sequence of calls and the exact order of setup steps.

The live experience is reachable, and the readme explains how entry source attribution works in it. There are two links. The existing link prefills a short starting prompt, and the application attributes that to developer_docs. A second, simplified DevX onboarding link prefills a different trial prompt, which is attributed to simplified_devx_onboarding. The point of the second link is that a first-time developer can be handed a link that already says something useful, rather than a bare QR code.

The repository is written in JavaScript, its primary language, and the manifest names the author as Meta.

One JSON event on stdout, and what it deliberately leaves out

The attribution design is the part of the sample worth studying, and it is unusually carefully specified.

The application writes one JSON event to stdout after the user sends either recognized phrase. AppRunner forwards stdout to CloudWatch Logs. Matching is case-sensitive after trimming surrounding whitespace, and other messages, including edited phrases and older tokenized formats, do not produce attribution events.

The event name is entry_source_attributed, and its fields are schema_version, source_surface, inbound_message_id, receiver_phone_number_id and event_timestamp. What it does not include is as informative as what it does: no sender phone number, and no raw message text. The readme says plainly that this measures attributed conversation starts, not QR scans or link opens.

That is a correct and careful distinction. A QR scan is a link open that may never become a conversation, and a link open may be a page load rather than a message. Counting the moment a user actually sends one of the two recognized phrases is counting something closer to what a business cares about, and it does so without putting user identifiers or message content into a log.

The two recognized phrases matter for the same reason. Only those exact messages, after trimming whitespace and matched case-sensitively, produce an event, so the metric cannot be inflated by a user typing an arbitrary message.

Why the deduplication key is the WAMID and not Redis

WhatsApp can retry the same webhook, and the readme gives a specific instruction about how to handle that: use inbound_message_id to deduplicate events instead of adding Redis state to the message path.

That is a design decision worth pausing on, because the sample does depend on Redis for other things, and the instinct in a Node application is to reach for Redis. The instruction is to not do that here. The webhook delivery id is already in the event payload, and a retried delivery of the same message carries the same inbound message id, so the deduplication key is already there. Adding a Redis write to the message path would mean every inbound message now depends on a round trip to a second service, for a key you were handed.

The readme then supplies a CloudWatch Logs Insights query that operationalises this, returning one row per inbound WAMID while retaining the delivery count:

text
fields @timestamp, event_timestamp, source_surface,
       inbound_message_id, receiver_phone_number_id
| filter event_name = "entry_source_attributed"
| stats latest(@timestamp) as last_seen,
        count(*) as webhook_delivery_count,
        latest(event_timestamp) as event_timestamp,
        latest(source_surface) as source_surface,
        latest(receiver_phone_number_id) as receiver_phone_number_id
  by inbound_message_id
| sort last_seen desc

The grouping by inbound_message_id collapses the retries into one row, and the count(*) as webhook_delivery_count column keeps the retry count visible rather than discarding it. So you can see both how many distinct conversations were attributed and how often the webhook was retried for each.

The dependency list in package.json includes redis at 5.8.2, and the setup instructions include installing and starting a Redis daemon, so Redis is used elsewhere in the app. It simply is not used in this path.

The Meta setup sequence, in the order the readme gives it

The setup section is the part most teams will actually need, and the readme is explicit about the order and about which steps are prerequisites.

Four things are required. A Meta Developer Account, which is needed to create new apps, created through the Get Started button on the Meta Developers website. A Meta App, which contains the settings for the WhatsApp automation including access tokens. Meta Business, which is a prerequisite for building with WhatsApp and can be created in the app creation flow. And a WhatsApp Business Account, which is needed to send and receive messages, created in Meta Business Suite.

Then the identifiers are collected. From the app Basic Settings you save the App ID and the App Secret. From Meta Business Suite, under the WhatsApp tab, you find the desired WhatsApp Business Account and save the Waba ID. Then you create a system user token for the app and save it as the App token, with a link to the instructions for acquiring an access token using a system user.

Finally, WhatsApp access is granted to the app: on the app Dashboard, under Add Product, find WhatsApp and click Set Up, land in the app's WhatsApp Settings, and go to the Configuration tab.

The reason for that final step is the part that catches people out. Access tokens are scoped by configuration, and the Configuration tab is where the app is told which phone number and which business account it is allowed to send as. Skipping it produces a registered app that cannot send anything.

Templates first, then runtime media, and the 30 day expiry

Two setup steps have a timing dependency that the readme spells out.

Templates come first. In order for the app to send templated messages, the templates must be created under the WhatsApp Business Account, either by running the template.sh script or through WhatsApp Manager.

Runtime media comes second, and this is where the subtlety is. The readme notes that template example handles are only used while creating templates, whereas runtime sends require media ids uploaded for the phone number that sends the messages. Each image used by the templates is uploaded through the WhatsApp Cloud API media endpoint for the sender phone number, and the returned ids are saved in .env as GROCERIES_MEDIA_ID, STRAWBERRIES_MEDIA_ID, SHEET_PAN_DINNER_MEDIA_ID and SALAD_BOWL_MEDIA_ID. The instruction is to use assets you own and not to use Asset Manager urls or the upload-session handles printed by template.sh as runtime media ids.

The lifetime constraint follows from that: media ids are scoped to the phone number that uploaded them and expire after 30 days. The remedy is to re-upload the assets, update all four environment variables, and restart the application before they expire.

So a deployment of this sample has a monthly maintenance task attached to it, and one that fails in a quiet way, since an expired media id produces a failed send rather than an obvious error. Anyone taking the pattern should treat the four variables as rotating credentials.

Running it locally: a tunnel, Redis, and a Node service

The local setup is a small stack of four pieces, and the readme walks through each.

The clone is the first step:

bash
$ git clone [email protected]:fbsamples/whatsapp-business-jaspers-market.git
$ cd whatsapp-business-jaspers-market

The requirements are Node 18.19 or higher, and either a remote server service, a local tunneling service such as ngrok, or your own webserver. The tunnel is not optional in practice, because Meta has to reach your webhook over the public internet.

Redis is started as a daemon:

bash
redis-server --daemonize yes

ngrok is installed globally with npm install -g ngrok, and then a tunnel is requested for the port:

bash
ngrok http 8080

The status output shows which public URL is forwarded to your local server, and the readme tells you to note the https URL of the external server, since that is the address to configure in the Meta app. The sample output in the readme shows the session status, the account, the version, the region, the web interface address, and the two forwarding lines, followed by the connections table with the ttl, opn, rt1, rt5, p50 and p90 columns.

The dependencies are installed with npm install, and the manifest shows a start script of node app.js, with the main entry point being app.js and the package type set to commonjs. The test script is node --test test/*.test.js, using the built-in Node test runner rather than an external framework.

The dependency list tells you what the sample actually talks to

The package.json is short enough to read as a summary of the architecture.

Five runtime dependencies. body-parser 1.20.2 and express 4.18.2 are the HTTP layer, with body-parser handling the webhook payload that Meta posts. dotenv 16.0.3 loads the.env file, which is where the four media ids and presumably the access tokens live, and the repository ships a.sample.env for that purpose. facebook-nodejs-business-sdk 23.0.1 is the Meta SDK dependency the app uses to talk to the WhatsApp Business Platform, and redis 5.8.2 is the cache or session store used by the app outside the attribution path.

The repository layout matches. app.js at the root is the entry point, services/ holds the business logic, test/ holds the tests, template.sh is the script that creates the message templates, and.sample.env is the environment template.

Two things are worth flagging for anyone evaluating this as a starting point. The manifest declares the licence as BSD while the repository landing page records Apache-2.0, so the licensing of the code needs checking against the LICENSE file in the root rather than either label. And there is no abstraction layer: this is a Node service for one fictional grocery brand, with the message flows and product catalogue written for that brand, not a client library you would extend.

What you take from it is the sequence and the shape of the calls, and the design decisions such as the stdout attribution event and the WAMID deduplication. The last push to the repository was on 2026-09-24 and there are no GitHub releases.

Editorial conclusion

Adopt this sample if you are building a WhatsApp Business integration and want a working reference for the whole path, from the Meta app and business account setup through template creation, runtime media upload, the tunnel, the Express webhook and the CloudWatch attribution event. Do not adopt it as a framework, because it is a sample application for one fictional brand with no abstraction layer over the WhatsApp API, and the four media id environment variables are hard-wired to that brand's assets. Before you build on the pattern, upload your own media and save the returned ids to .env, since media ids are scoped to the uploading phone number and expire after 30 days, and decide where you will receive stdout, because the app writes its attribution event there and nothing in the sample collects it for you.

Frequently asked questions

What is the Jasper's Market WhatsApp sample app?

It is a fictional grocery brand created to demonstrate key features of the WhatsApp Business Platform, with a bot that shows the platform end to end. The readme frames it as inspiration for building a WhatsApp experience that combines automation with live customer support.

What does the entry_source_attributed event contain?

The fields are schema_version, source_surface, inbound_message_id, receiver_phone_number_id and event_timestamp, written as one JSON event to stdout. It does not include the sender phone number or the raw message text, and it measures attributed conversation starts rather than QR scans or link opens.

How should duplicate WhatsApp webhook deliveries be handled?

By using inbound_message_id to deduplicate events, and not by adding Redis state to the message path, because WhatsApp can retry the same webhook. The readme includes a CloudWatch Logs Insights query that groups by inbound_message_id and keeps count(*) as webhook_delivery_count, so one row appears per WAMID with the retry count retained.

What must I do before the Jasper's Market app can send messages?

You need a Meta Developer Account, a Meta App, a Meta Business and a WhatsApp Business Account, then the App ID, App Secret, Waba ID and a system user App token. Finally, on the app Dashboard, add the WhatsApp product, go to Set Up, and configure it in the Configuration tab, which is what lets the app send as the chosen number.

Why do the runtime media ids in the Jasper's Market sample expire?

Media ids are scoped to the phone number that uploaded them and expire after 30 days. The remedy stated in the readme is to re-upload the assets, update GROCERIES_MEDIA_ID, STRAWBERRIES_MEDIA_ID, SHEET_PAN_DINNER_MEDIA_ID and SALAD_BOWL_MEDIA_ID in .env, and restart the application.

Official sources

  1. fbsamples/whatsapp-business-jaspers-market on GitHub
  2. Issues
  3. License: Apache-2.0
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/fbsamples-whatsapp-business-jaspers-market.svg)](https://hysenlabs.com/projects/fbsamples-whatsapp-business-jaspers-market)

Was this useful?