Receive Messenger Webhooks Locally
Receive signed Messenger webhooks locally through a temporary HTTPS tunnel.
Introduction
30 Second Summary
Online services can send updates at any moment, even while the app receiving them runs only on your computer. The difficult part is giving that service a secure address without opening an inbound port on your machine.
In this project, you will build a small Messenger Platform webhook receiver using FastAPI behind a temporary Cloudflare Tunnel. You will watch a signed test event cross the public address into a service that remains bound to your computer.
What You'll Build
You'll trigger a Messenger Platform test event from Meta's dashboard and watch your local PowerShell terminal print the verified JSON payload received through a temporary HTTPS callback.
By the end of this project, you'll have:
- Local verification: Open a callback in your browser to see 12345 returned from the local receiver.
- Minimal public surface: Run the receiver on 127.0.0.1:8000 with its documentation routes disabled. Visit /docs to confirm it returns 404 Not Found.
- Signed delivery: Send a Messenger test notification through the temporary callback. Watch the receiver validate its HMAC SHA-256 signature before printing the payload.
- Secret Mission: Turn the raw Messenger payload into a compact sender-and-message summary.
Are there any prerequisites?
You'll need a Windows computer running VS Code with Python 3.11.4. You also need a Meta developer app with Messenger Platform enabled plus access to its App Secret.
A Facebook Page is optional for the dashboard test event, but it is required if you later connect real Page messages.
Before We Start
Before the hands-on work begins, commit to the local-first Messenger webhook design you are building and why its boundary matters.
Prepare the Local Python Project
Your Messenger Platform webhook needs an isolated Python environment so its packages do not clash with other projects. The Meta App Secret also needs to stay outside every project file.
This step creates messenger-webhook-local in VS Code with exact dependency pins. requirements.txt pins FastAPI 0.141.1 plus Uvicorn 0.52.4.
A Python virtual environment keeps those packages inside this project. The current Windows PowerShell process holds the webhook values outside the codebase.
In this step, get ready to:
- Create a local project with pinned Python dependencies.
- Build .venv with the pinned packages installed.
- Keep the webhook values in the current PowerShell session.
Create the project files
The project folder gives every local file one predictable home. The requirements file records the exact package versions that the webhook uses.
- Press the Windows key to open the Windows search bar.
- Type VS Code and press Enter to open it.
- Click File in the top menu bar.
- Click Open Folder.
- Choose your Desktop in the folder picker.
- Click New folder.
- Enter messenger-webhook-local as the folder name.
- Click Select Folder.
That first piece is in place: VS Code now shows the empty messenger-webhook-local folder in the Explorer sidebar.
- Click the New File icon in the Explorer sidebar.
- Enter requirements.txt as the file name.
- Add the exact dependency pins by copying this code into requirements.txt:
fastapi==0.141.1
uvicorn==0.52.4
What Does This File Control?
- The fastapi==0.141.1 line selects the web framework version used by the receiver.
- The uvicorn==0.52.4 line selects the server version that runs the receiver locally.
- Exact pins keep future installs aligned with the versions used throughout this project.
- Save requirements.txt by pressing Ctrl+S.
- Confirm the Explorer sidebar lists requirements.txt inside messenger-webhook-local.
Can't See requirements.txt?
- Check that the Explorer sidebar shows messenger-webhook-local as the open folder.
- Check that the file name ends with .txt.
- Save the file again with Ctrl+S if the editor tab still shows an unsaved-change indicator.
Help me find or save requirements.txt in my VS Code project.
✔️ Awesome, I've got everything!
Your project folder now contains the saved dependency file. Keep messenger-webhook-local open in VS Code for the next substep.
ⓧ I'd like to double check the full code
Your complete requirements.txt file should match this reference exactly.
fastapi==0.141.1
uvicorn==0.52.4
What Should Match?
Compare both package names with both version pins. Extra characters can change what the installer reads.
Isolate the Python dependencies
The virtual environment gives this webhook its own Python executables plus package directory. Calling the executables inside .venv keeps each command tied to the project environment.
- Click Terminal in the VS Code top menu.
- Click New Terminal.
- Confirm the terminal profile label shows PowerShell.
- Create .venv with the pinned dependencies by running these commands:
python -m venv .venv
./.venv/Scripts/pip.exe install -r requirements.txt
What Do These Commands Do?
- The first command uses Python's venv module to create an isolated environment at .venv.
- The second command uses the environment's own pip.exe to read requirements.txt.
- Using executables inside .venv guarantees that later commands use the installed project packages.
The terminal displays package installation progress. The final output confirms that the pinned dependencies were installed.
Did the Installation Fail?
- Confirm the terminal path ends with messenger-webhook-local.
- Confirm requirements.txt contains both lines from the full-code reference.
- Confirm the .venv folder appears in the Explorer sidebar after the first command finishes.
Help me troubleshoot the virtual environment or dependency installation.
Keep webhook secrets in PowerShell
The receiver later reads VERIFY_TOKEN during Meta's verification request. It reads APP_SECRET when checking signed event notifications.
This part handles a live credential, so take it slowly: the value stays in the current PowerShell process. It never enters a project file.
- Open your Meta developer app in the Meta App Dashboard.
- Select Settings in the dashboard menu.
- Select Basic.
- Copy the App Secret without placing it in a file.
The next assignments exist only in the PowerShell process that receives them. Closing that terminal clears the values from the session.
- Return to the VS Code PowerShell terminal from earlier.
- Choose a private random string that you can re-enter during Meta verification.
- Replace replace-with-your-own-random-string with your random verification string.
- Replace paste-your-meta-app-secret with the App Secret you copied.
- Set both process-scoped values by adapting these commands before pressing Enter:
$Env:VERIFY_TOKEN = "replace-with-your-own-random-string"
$Env:APP_SECRET = "paste-your-meta-app-secret"
Why Use the Current Session?
- PowerShell stores both values in the current process instead of requirements.txt.
- The server started from this terminal can read the values later.
- Closing PowerShell removes these process-scoped values from the session.
PowerShell returns to the prompt after each assignment. Your project files remain free of both values.
Were the Values Rejected?
- Keep the double quotes around each value.
- Run both assignments in the same PowerShell terminal that you use for the server later.
- Avoid pasting either value into requirements.txt or any other project file.
Help me set process-scoped webhook values safely in PowerShell.
Before you run the final check, think about the two version numbers you expect to see.
- Verify the isolated Python and Uvicorn executables by running these commands:
./.venv/Scripts/python.exe --version
./.venv/Scripts/uvicorn.exe --version
What Does This Check Prove?
- The first command asks the Python executable inside .venv for its version.
- The second command asks the Uvicorn executable installed inside the same environment for its version.
- Both results come from the isolated project environment.
You should see output that includes Python 3.11.4 plus Uvicorn 0.52.4. That confirms the local Python foundation is ready.
Don't See Both Versions?
- Confirm the terminal path ends with messenger-webhook-local.
- Confirm the .venv folder contains a Scripts folder.
- Run the dependency installation commands again if the Uvicorn executable is missing.
Help me fix the .venv version checks in Windows PowerShell.
Your isolated environment now has the exact packages it needs. Next, you will build the local receiver that uses these process-scoped values.
Build and Test the Local Webhook Receiver
Your isolated Python environment is ready. A reliable Messenger Platform callback needs a working localhost receiver before any public exposure.
FastAPI will process both webhook request paths. Uvicorn will keep the receiver bound to your computer while you test its behavior.
In this step, get ready to:
- Configure a FastAPI app with its documentation routes disabled.
- Implement both Meta webhook request paths.
- Prove the receiver works on localhost.
Create the minimal public surface
FastAPI normally publishes interactive documentation for an application. This receiver disables those discovery routes so the local service exposes only the webhook operations you add.
- Create main.py inside messenger-webhook-local by using the new-file control in the VS Code Explorer sidebar.
You should see main.py beside requirements.txt in the Explorer sidebar.
- Add the initial application configuration to main.py by pasting this code:
import hashlib
import hmac
import json
import os
from fastapi import FastAPI, Request
from fastapi.responses import PlainTextResponse
app = FastAPI(openapi_url=None, docs_url=None, redoc_url=None)
VERIFY_TOKEN = os.getenv("VERIFY_TOKEN")
APP_SECRET = os.getenv("APP_SECRET")
if not VERIFY_TOKEN or not APP_SECRET:
raise RuntimeError("Set VERIFY_TOKEN and APP_SECRET before starting the server.")
What does this setup do?
- The imports provide request handling plus the cryptographic tools required for signed notifications.
- The FastAPI configuration disables the OpenAPI schema plus both documentation interfaces.
- os.getenv() reads the two process-scoped values from your current PowerShell session.
- The startup guard stops the server when either required value is missing.
- Save main.py by pressing Ctrl+S.
- Start the local receiver from the current VS Code PowerShell terminal by running this command:
./.venv/Scripts/uvicorn.exe main:app --host 127.0.0.1 --port 8000 --reload
Why bind to localhost?
The --host 127.0.0.1 setting accepts connections from your computer. The service does not listen on your network interfaces.
The --reload setting restarts the application when you save main.py. This keeps the edit-and-test loop short.
- Open a browser tab.
- Load http://127.0.0.1:8000/docs from the address bar.
The browser displays 404 Not Found. That response confirms the documentation route is disabled.
Great. Your local server is running with its documentation routes closed.
Server not starting?
Check that you ran the command in the current VS Code PowerShell session. Your process-scoped values exist in that session.
Confirm that main.py is saved inside messenger-webhook-local. A missing value triggers Set VERIFY_TOKEN and APP_SECRET before starting the server.
Help me diagnose why my local FastAPI server is not starting.
Implement Meta's request paths
Meta begins webhook setup with an HTTP GET request. Your handler must confirm the mode plus your verification value before returning Meta's challenge.
- Switch back to main.py in VS Code.
- Add the GET handler two blank lines below the startup guard by pasting this code:
@app.get("/webhook", response_class=PlainTextResponse)
async def verify_webhook(request: Request):
mode = request.query_params.get("hub.mode")
token = request.query_params.get("hub.verify_token")
challenge = request.query_params.get("hub.challenge")
if mode == "subscribe" and token == VERIFY_TOKEN and challenge:
print("Webhook verification accepted.")
return challenge
return PlainTextResponse("Forbidden", status_code=403)
How does verification work?
- mode must contain subscribe for a verification request.
- token must match the process-scoped VERIFY_TOKEN value.
- challenge is returned as plain text after both checks pass.
- A failed check receives Forbidden with status code 403.
- Save main.py by pressing Ctrl+S.
Before you test the handler, predict whether the matching mode plus verification value will return the challenge.
- Test the GET handler by copying this address into your browser's address bar:
http://127.0.0.1:8000/webhook?hub.mode=subscribe&hub.verify_token=replace-with-your-own-random-string&hub.challenge=12345
What does this address test?
The query string sends subscribe as the mode. It also sends a verification value plus the challenge 12345.
The handler compares the supplied value with VERIFY_TOKEN before returning the challenge.
- Replace replace-with-your-own-random-string in the browser address bar with the same random verification string stored in VERIFY_TOKEN.
- Press Enter to send the local verification request.
Your browser displays 12345. PowerShell prints Webhook verification accepted.
You have now proven the verification handshake entirely on your computer.
Challenge not appearing?
Confirm that the address contains hub.mode=subscribe. Check that the verification value matches the current VERIFY_TOKEN value exactly.
Make sure Uvicorn reloaded after you saved main.py. The PowerShell terminal should remain occupied by the running server.
Help me fix my local webhook verification request.
Event notifications use a signed POST body. The receiver computes its own HMAC SHA-256 value from the untouched body before trusting the payload.
- Switch back to main.py in VS Code.
- Add the POST handler two blank lines below the GET handler by pasting this code:
@app.post("/webhook", response_class=PlainTextResponse)
async def receive_webhook(request: Request):
body = await request.body()
signature = request.headers.get("X-Hub-Signature-256", "")
expected_signature = "sha256=" + hmac.new(
APP_SECRET.encode("utf-8"),
body,
digestmod=hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(signature, expected_signature):
print("Rejected event with invalid signature.")
return PlainTextResponse("Forbidden", status_code=403)
payload = await request.json()
print("Verified Messenger event:")
print(json.dumps(payload, indent=2))
return "EVENT_RECEIVED"
How are events protected?
- body holds the raw request bytes used for signature calculation.
- expected_signature combines the sha256= prefix with the HMAC digest created from APP_SECRET.
- hmac.compare_digest() compares the supplied X-Hub-Signature-256 value with the expected signature.
- The receiver parses the JSON payload after signature validation succeeds.
- A verified event is printed before the handler returns EVENT_RECEIVED.
- Save main.py by pressing Ctrl+S.
You should see Uvicorn finish reloading without a Python traceback. This confirms the complete file can be imported.
- Refresh the verification URL from earlier.
The browser still displays 12345. The successful response confirms the GET path still works after adding signature validation.
Server failing after the POST handler?
Check that the @app.post decorator begins two blank lines below the GET handler. Make sure every line inside receive_webhook() uses the indentation shown above.
Confirm that expected_signature includes the sha256= prefix. The comparison requires the same format as the request header.
Help me debug the POST webhook handler in main.py.
✔️ Awesome, I've got everything!
Your main.py file is saved. Uvicorn should still be running in the current PowerShell terminal.
ⓧ I'd like to double check the full code
Compare your saved main.py file with this complete version.
import hashlib
import hmac
import json
import os
from fastapi import FastAPI, Request
from fastapi.responses import PlainTextResponse
app = FastAPI(openapi_url=None, docs_url=None, redoc_url=None)
VERIFY_TOKEN = os.getenv("VERIFY_TOKEN")
APP_SECRET = os.getenv("APP_SECRET")
if not VERIFY_TOKEN or not APP_SECRET:
raise RuntimeError("Set VERIFY_TOKEN and APP_SECRET before starting the server.")
@app.get("/webhook", response_class=PlainTextResponse)
async def verify_webhook(request: Request):
mode = request.query_params.get("hub.mode")
token = request.query_params.get("hub.verify_token")
challenge = request.query_params.get("hub.challenge")
if mode == "subscribe" and token == VERIFY_TOKEN and challenge:
print("Webhook verification accepted.")
return challenge
return PlainTextResponse("Forbidden", status_code=403)
@app.post("/webhook", response_class=PlainTextResponse)
async def receive_webhook(request: Request):
body = await request.body()
signature = request.headers.get("X-Hub-Signature-256", "")
expected_signature = "sha256=" + hmac.new(
APP_SECRET.encode("utf-8"),
body,
digestmod=hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(signature, expected_signature):
print("Rejected event with invalid signature.")
return PlainTextResponse("Forbidden", status_code=403)
payload = await request.json()
print("Verified Messenger event:")
print(json.dumps(payload, indent=2))
return "EVENT_RECEIVED"
Verify the local receiver
The final check proves that the intended route responds while the documentation surface stays closed. Uvicorn remains bound to 127.0.0.1:8000 throughout both checks.
Before the final check, predict which browser page will show the challenge plus which page will return an error.
- Return to the local verification browser tab.
- Refresh the page.
Your browser displays 12345. PowerShell prints Webhook verification accepted.
- Open a separate browser tab.
- Load http://127.0.0.1:8000/docs from the address bar.
The documentation address returns 404 Not Found. Your local receiver now exposes the intended webhook path without FastAPI's documentation endpoints.
Keep Uvicorn running
Leave the current PowerShell terminal open with Uvicorn running. The next step uses this same local receiver to test an external callback limitation.
- Crop the browser address bar from your verification screenshot so the verification value stays out of view.
- Keep any secret-setting commands outside the terminal area you capture.
Your localhost receiver now passes its local verification checks. Next, you will test whether Meta can reach the same callback.
Prove the Localhost Limitation
Your FastAPI receiver now answers an HTTP GET verification request in your own browser. That proves the route works inside your computer.
A Messenger Platform callback must also be reachable from outside your computer over HTTPS. This step puts the localhost address into Meta's real callback flow to test that boundary.
In this step, get ready to:
- Locate the Messenger webhook configuration for your existing Meta app.
- Submit the local callback address for verification.
- Compare Meta's external check with the successful local check.
Open the Messenger webhook settings
The webhook settings connect your existing app to the endpoint that receives verification requests. You only need the two callback fields for this test.
- In your browser, open the Meta App Dashboard for the developer app you used earlier.
- Select Products in the left sidebar.
- Select Messenger for your existing app.
- Select Settings.
- Find the Callback URL field.
- Find the Verify Token field.
You now have the two fields Meta uses to send its verification request.
Attempt the local callback
Your local route already returns the correct challenge. The dashboard test now checks whether the same address works from Meta's side.
- Enter http://127.0.0.1:8000/webhook in the Callback URL field.
- Enter the same verification string stored in VERIFY_TOKEN in the Verify Token field.
Before you save, do you think Meta can reach the same address that your browser reached?
- Click Save to attempt the callback verification.
Meta does not mark the callback as verified. This is the intended shortfall in the test.
Why the Local Callback Fails
The loopback address 127.0.0.1 always points back to the computer making the request. From Meta's servers, it does not point to your Windows computer.
Meta requires a public HTTPS webhook endpoint for verification. Your Uvicorn process listens only on your local machine.
Confirm the boundary from both sides
The code and server have stayed unchanged. Rechecking the local route separates a network reachability problem from an application bug.
Before you refresh, do you expect the local verification route to succeed again?
- Leave the Uvicorn terminal from earlier running.
- Switch back to the local verification page from earlier.
- Refresh the page.
You'll still see the same challenge value in your browser. PowerShell prints Webhook verification accepted. again.
That contrast is the proof you needed: the receiver works locally, but Meta cannot reach its loopback address. Next, you will add a temporary HTTPS path while Uvicorn stays bound to localhost.
Open a Temporary HTTPS Boundary
Your FastAPI receiver now verifies requests on localhost. However, Messenger Platform still has no public HTTPS address it can call.
A Cloudflare Quick Tunnel creates that boundary through an outbound connection. Your server stays bound to 127.0.0.1:8000, so you do not need a VPN, public IP, inbound port, firewall rule, or router change.
In this step, get ready to:
- Add the Windows Cloudflare connector at version 2026.8.3.
- Start a Quick Tunnel from a second PowerShell terminal.
- Prove the public HTTPS address reaches the local Uvicorn receiver.
Add the pinned Windows connector
The standalone cloudflared.exe connector keeps this setup inside your project folder. Version 2026.8.3 ensures your connector matches the project.
- Leave the existing Uvicorn PowerShell terminal running.
- Click Terminal in the top menu of VS Code.
- Select New Terminal to create a second PowerShell terminal.
- Check whether the connector already exists in messenger-webhook-local by running this command:
./cloudflared.exe --version
What does this command check?
The --version option asks the local executable to identify its installed release. The result tells you whether the required connector is present.
The result matches one of the three paths below.
Choosing between the two Windows files is the fiddly part because the release includes separate assets for 64-bit and 32-bit systems.
✔️ I see the required version
Your connector already reports version 2026.8.3. The pinned executable is ready.
ⓧ I see an older version
Windows may ask whether to replace the existing file when you move the new copy.
- Open the official Cloudflare release page for version 2026.8.3.
- Download cloudflared-windows-amd64.exe if your Windows computer is 64-bit.
- Download cloudflared-windows-386.exe if your Windows computer is 32-bit.
- Rename the downloaded file to cloudflared.exe.
- Drag the renamed file from your Downloads folder into the top level of messenger-webhook-local in the VS Code Explorer sidebar.
- Confirm the replacement if Windows displays a file replacement prompt.
ⓧ Command not found
The connector is not inside messenger-webhook-local yet. The standalone Windows asset supplies it without changing your system path.
- Open the official Cloudflare release page for version 2026.8.3.
- Download cloudflared-windows-amd64.exe if your Windows computer is 64-bit.
- Download cloudflared-windows-386.exe if your Windows computer is 32-bit.
- Rename the downloaded file to cloudflared.exe.
- Drag the renamed file from your Downloads folder into the top level of messenger-webhook-local in the VS Code Explorer sidebar.
- Confirm the connector now matches the pinned release by running this command in the second PowerShell terminal:
./cloudflared.exe --version
What does this verification prove?
This runs the executable directly from messenger-webhook-local. It proves the renamed file is in the location used by the next command.
You should see version output containing 2026.8.3.
Connector still not found?
- Check that the file is named exactly cloudflared.exe in the VS Code Explorer sidebar.
- Check that the second PowerShell terminal is inside messenger-webhook-local.
- Download the asset that matches your Windows architecture if the executable does not start.
Ask for help with checking why cloudflared.exe does not run from my project folder.
Start the Quick Tunnel
A Quick Tunnel generates a temporary trycloudflare.com hostname. The running connector forwards requests from that hostname to your local receiver.
The tunnel process occupies the second terminal while it runs. Your original terminal continues running Uvicorn with the webhook secrets already loaded.
- Forward the temporary HTTPS address to the running local receiver by running this command:
./cloudflared.exe tunnel --url http://localhost:8000
What does this command do?
The connector creates an outbound tunnel to Cloudflare. The --url http://localhost:8000 portion directs incoming requests to the Uvicorn server on your computer.
Cloudflare supplies the public HTTPS side of the connection. Your local server remains bound to 127.0.0.1:8000.
The second terminal prints a generated address ending in trycloudflare.com.
- Copy the generated address into https://your-random-subdomain.trycloudflare.com.
- Leave the second PowerShell terminal running to keep the temporary address active.
Keep It for Development
Quick Tunnels are intended for testing and development. They have no service-level agreement, so this temporary hostname is a development callback instead of production hosting.
No Temporary Address?
- Check that your computer has an active internet connection.
- Check that cloudflared.exe is still running in the second PowerShell terminal.
- Check that Uvicorn remains active in the original PowerShell terminal.
Ask for help with troubleshooting a Quick Tunnel that does not generate a trycloudflare.com address.
Prove the public boundary reaches localhost
The generated hostname now gives your existing GET verification route a temporary public entrance. A successful request proves the outbound tunnel reaches the same receiver that remains local.
- Switch back to the browser from your earlier local verification test.
- Paste https://your-random-subdomain.trycloudflare.com into the address bar.
- Append /webhook?hub.mode=subscribe&hub.verify_token=replace-with-your-own-random-string&hub.challenge=67890 to the address.
- Replace replace-with-your-own-random-string with the exact value stored in VERIFY_TOKEN.
Before you send the request, do you expect the temporary HTTPS address to reach a server that still listens only on localhost?
- Press Enter to send the verification request.
The browser displays 67890. The Uvicorn terminal prints Webhook verification accepted. while the server remains bound to 127.0.0.1:8000.
That is the boundary proven. A public HTTPS request has crossed the temporary tunnel into your localhost-only receiver.
Challenge Not Displayed?
- Check that both PowerShell terminals are still running.
- Check that the browser address starts with the generated https:// hostname.
- Check that the verification token matches the value stored in VERIFY_TOKEN exactly.
Ask for help with debugging why my Quick Tunnel verification URL does not return the challenge.
✔️ Awesome, I've got everything!
Your files remain saved. The connector is present at version 2026.8.3, and the temporary HTTPS boundary reaches your local receiver.
ⓧ I'd like to double check the full code
The tunnel runs as a separate process, so main.py remains unchanged in this step.
import hashlib
import hmac
import json
import os
from fastapi import FastAPI, Request
from fastapi.responses import PlainTextResponse
app = FastAPI(openapi_url=None, docs_url=None, redoc_url=None)
VERIFY_TOKEN = os.getenv("VERIFY_TOKEN")
APP_SECRET = os.getenv("APP_SECRET")
if not VERIFY_TOKEN or not APP_SECRET:
raise RuntimeError("Set VERIFY_TOKEN and APP_SECRET before starting the server.")
@app.get("/webhook", response_class=PlainTextResponse)
async def verify_webhook(request: Request):
mode = request.query_params.get("hub.mode")
token = request.query_params.get("hub.verify_token")
challenge = request.query_params.get("hub.challenge")
if mode == "subscribe" and token == VERIFY_TOKEN and challenge:
print("Webhook verification accepted.")
return challenge
return PlainTextResponse("Forbidden", status_code=403)
@app.post("/webhook", response_class=PlainTextResponse)
async def receive_webhook(request: Request):
body = await request.body()
signature = request.headers.get("X-Hub-Signature-256", "")
expected_signature = "sha256=" + hmac.new(
APP_SECRET.encode("utf-8"),
body,
digestmod=hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(signature, expected_signature):
print("Rejected event with invalid signature.")
return PlainTextResponse("Forbidden", status_code=403)
payload = await request.json()
print("Verified Messenger event:")
print(json.dumps(payload, indent=2))
return "EVENT_RECEIVED"
Why Is This File Unchanged?
The Quick Tunnel forwards network traffic without changing your webhook logic. The GET and POST handlers continue applying the same verification checks.
Your pinned Python packages also remain unchanged.
fastapi==0.141.1
uvicorn==0.52.4
What Should the Project Contain?
The requirements.txt file still pins FastAPI and Uvicorn. Your project folder now also contains cloudflared.exe at version 2026.8.3.
The original PowerShell terminal should still run Uvicorn. The second PowerShell terminal should still show the active Quick Tunnel.
Your localhost-only receiver now has a temporary HTTPS callback. Next, you will connect Messenger Platform to this address and watch a signed event arrive in your local terminal.
Connect Messenger and Receive an Event
Your Cloudflare Quick Tunnel has proved that a temporary HTTPS address can reach the FastAPI receiver on your computer. The service still listens only on 127.0.0.1:8000.
Messenger Platform now needs that address as its verified callback. This step completes the connection by delivering a signed event to your local process.
In this step, get ready to:
- Verify the temporary HTTPS callback with Meta.
- Enable the messages subscription.
- Send a signed Event Notification to your local receiver.
Verify the callback with Meta
Meta verifies a callback by sending an HTTP GET request containing the mode, verification token, and challenge. Your receiver accepts the request only when the supplied token matches VERIFY_TOKEN.
- In your Meta App Dashboard, select Products from the app sidebar.
- Select Messenger.
- Select Settings.
- Switch back to the second PowerShell terminal from earlier.
- Copy the generated Quick Tunnel HTTPS address here: https://<random-subdomain>.trycloudflare.com.
- Enter https://<random-subdomain>.trycloudflare.com followed by /webhook in the Callback URL field.
- Enter the exact value stored in VERIFY_TOKEN in the Verify Token field.
- Click Save.
Meta sends its verification request through the tunnel. Your Uvicorn terminal prints Webhook verification accepted. when the callback succeeds.
That completes the public handshake. Meta can now reach your temporary callback without exposing an inbound port on your computer.
Callback verification did not succeed?
- Confirm that the Quick Tunnel process is still running in the second PowerShell terminal.
- Confirm that the Uvicorn process is still running in its original PowerShell terminal.
- Check that the Callback URL ends with /webhook.
- Check that the Verify Token exactly matches the value stored in VERIFY_TOKEN.
help me troubleshoot Messenger callback verification
Subscribe to incoming messages
A verified callback gives Meta a destination. The messages subscription tells Messenger Platform which type of event to deliver there.
- Return to the Messenger Settings page from the previous substep.
- Find messages in the webhook subscription fields.
- Enable the subscription control beside messages.
You should see messages shown as an enabled subscription. Meta can now send incoming-message test events to your callback.
Why use a dashboard test event?
The dashboard test Event Notification proves that the callback can receive Messenger payloads in development mode. This path does not require a Facebook Page.
Real Page messages require the messaging app to be installed on a Facebook Page. The dashboard test keeps this project focused on the webhook boundary.
Send a signed test event
The POST handler authenticates each request before it logs the JSON payload. It calculates an HMAC using SHA-256 from the raw request body and APP_SECRET.
Before the test runs, consider whether the receiver can print the payload before the signature comparison.
- Use the control beside messages to select Test.
- Select Send to My Server.
- Switch back to the Uvicorn PowerShell terminal from earlier.
Your terminal prints Verified Messenger event: followed by formatted JSON. The endpoint returns EVENT_RECEIVED with HTTP 200.
That is the full round trip complete: Meta's signed event crossed the temporary HTTPS boundary into your localhost-only receiver.
The Uvicorn process remains bound to 127.0.0.1:8000. Only the temporary Quick Tunnel address provides the public callback.
What just passed?
- receive_webhook reads the raw request body before parsing the payload.
- expected_signature contains the HMAC SHA-256 value derived from the request body and APP_SECRET.
- hmac.compare_digest compares that value with X-Hub-Signature-256.
- The handler prints the JSON only after the signatures match.
Test event did not reach Uvicorn?
- Confirm that both PowerShell processes are still running.
- Confirm that the verified Callback URL uses the current Quick Tunnel address.
- Confirm that messages remains enabled in Messenger Settings.
- If the terminal prints Rejected event with invalid signature. confirm that the Uvicorn session holds the App Secret for the same Meta app.
help me troubleshoot a Messenger test event that is missing or rejected
✔️ Awesome, I've got everything!
Your callback is verified. The messages subscription now delivers signed test events to your local receiver.
ⓧ I'd like to double check the full code
Your source files stay unchanged in this step. Compare them with these complete versions.
import hashlib
import hmac
import json
import os
from fastapi import FastAPI, Request
from fastapi.responses import PlainTextResponse
app = FastAPI(openapi_url=None, docs_url=None, redoc_url=None)
VERIFY_TOKEN = os.getenv("VERIFY_TOKEN")
APP_SECRET = os.getenv("APP_SECRET")
if not VERIFY_TOKEN or not APP_SECRET:
raise RuntimeError("Set VERIFY_TOKEN and APP_SECRET before starting the server.")
@app.get("/webhook", response_class=PlainTextResponse)
async def verify_webhook(request: Request):
mode = request.query_params.get("hub.mode")
token = request.query_params.get("hub.verify_token")
challenge = request.query_params.get("hub.challenge")
if mode == "subscribe" and token == VERIFY_TOKEN and challenge:
print("Webhook verification accepted.")
return challenge
return PlainTextResponse("Forbidden", status_code=403)
@app.post("/webhook", response_class=PlainTextResponse)
async def receive_webhook(request: Request):
body = await request.body()
signature = request.headers.get("X-Hub-Signature-256", "")
expected_signature = "sha256=" + hmac.new(
APP_SECRET.encode("utf-8"),
body,
digestmod=hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(signature, expected_signature):
print("Rejected event with invalid signature.")
return PlainTextResponse("Forbidden", status_code=403)
payload = await request.json()
print("Verified Messenger event:")
print(json.dumps(payload, indent=2))
return "EVENT_RECEIVED"
fastapi==0.141.1
uvicorn==0.52.4
Your Messenger webhook configuration should match this checkpoint:
- The Callback URL uses https://<random-subdomain>.trycloudflare.com followed by /webhook.
- The Verify Token matches the current PowerShell VERIFY_TOKEN value.
- Meta callback verification has succeeded.
- The messages subscription is enabled.
- Signed dashboard test Event Notifications reach the local receiver.
- The second PowerShell terminal still runs Cloudflare Tunnel version 2026.8.3.
- The Uvicorn process still listens only on 127.0.0.1:8000.
Secret mission
Extract the Sender and Message Text
Turn the verified Messenger payload into a one-line summary that shows the sender ID with its message text. Add a safe fallback for valid events that do not contain a message.
Clean Up Your Resources
Clean Up Your Resources
The development-only Cloudflare Quick Tunnel keeps this project at $0. It has no SLA.
Choose whether to keep your resources running, pause them for later, or delete them entirely.
Resources you used:
- The complete messenger-webhook-local project folder.
- The Uvicorn process serving your local FastAPI receiver at 127.0.0.1:8000.
- The cloudflared.exe process providing the temporary HTTPS callback.
- The process-scoped VERIFY_TOKEN plus APP_SECRET values in the current PowerShell session.
- The verified Messenger Platform callback configuration in Meta.
- The messages webhook subscription in Meta.
Keep everything running
No action is needed. Choose this if you are still testing the Messenger callback in the current session.
- Keep the Uvicorn PowerShell terminal open.
- Keep the cloudflared.exe PowerShell terminal open.
- Keep the messenger-webhook-local folder in its current location.
- Leave the verified callback configuration in Meta.
- Leave the messages subscription enabled.
The temporary callback works only while both processes run. It remains a development-only endpoint.
Pause - I'll come back to this later
Pausing frees the local port. Your project files remain ready for another development session.
- Switch back to the PowerShell terminal running cloudflared.exe.
- Press Ctrl+C to stop the Quick Tunnel.
- Switch back to the PowerShell terminal running Uvicorn.
- Press Ctrl+C to stop the local receiver.
- Close the Uvicorn PowerShell terminal to clear its process-scoped webhook secrets.
- Keep the messenger-webhook-local folder in its current location.
- Leave the Meta webhook configuration in place.
The Meta callback remains configured. Its temporary hostname stops responding after the Quick Tunnel closes.
- Reload VERIFY_TOKEN when you return in a fresh PowerShell session.
- Reload APP_SECRET in the same PowerShell session.
- Restart the Uvicorn receiver from the messenger-webhook-local folder.
- Start a new Quick Tunnel from the second PowerShell terminal.
- Replace the Meta callback URL with the newly generated HTTPS address plus /webhook.
- Complete Meta callback verification with the same Verify Token.
Delete - I don't want to use this again
Remove every project resource if you want a clean starting point. Begin with the external callback configuration.
- Return to your app in the Meta App Dashboard.
- Select Products.
- Select Messenger.
- Select Settings.
- Remove the messages subscription.
- Remove the webhook configuration containing the temporary callback URL.
That closes the external side. Meta no longer targets the temporary hostname.
- Switch back to the PowerShell terminal running cloudflared.exe.
- Press Ctrl+C to stop the Quick Tunnel.
- Switch back to the PowerShell terminal running Uvicorn.
- Press Ctrl+C to stop the local receiver.
- Close the Uvicorn PowerShell terminal to clear VERIFY_TOKEN plus APP_SECRET.
The Quick Tunnel created no named tunnel. There is no DNS record or Cloudflare account configuration to remove.
- Close VS Code.
- Use Windows File Explorer to return to the location where you created messenger-webhook-local.
- Delete the messenger-webhook-local folder permanently.
- Confirm the deletion if Windows asks for approval.
You should no longer see the messenger-webhook-local folder in its previous location. Your local receiver files are now removed.
Nice Work!
Nice Work!
Outstanding work! Your localhost-only FastAPI receiver now accepts signed Messenger Platform test events through a temporary Cloudflare Quick Tunnel.
You've learned how to:
- Build a minimal webhook receiver that stays bound to 127.0.0.1:8000 with FastAPI's documentation routes disabled.
- Implement Meta webhook verification by checking hub.mode plus hub.verify_token before returning hub.challenge.
- Deliver signed Messenger events through a temporary HTTPS callback after validating X-Hub-Signature-256 with HMAC SHA-256.
- Secret Mission: Use summarize_message() to print a sender-and-message summary from verified Messenger payloads.
Ready to quiz yourself?