How to Send WhatsApp Messages with Python and Twilio
Python project that fetches real-time currency exchange rates and sends them as WhatsApp notifications using the Twilio API. Step-by-step setup with cron scheduling.

Have you ever needed to stay updated with real-time currency exchange rates and receive instant notifications? If so, I have an interesting project to share. Introducing the Currency Exchange Rate to BRL WhatsApp Notifier, a Python-based tool that fetches current exchange rates for various currencies and cryptocurrencies, converts them to Brazilian Real (BRL) or others, and sends the converted rates directly to your WhatsApp or group using the Twilio API.
In this article I go beyond “clone and run.” I walk through the design decisions that actually matter when you want a bot like this to stay up for months without babysitting: which exchange-rate API to pick, how to structure the code, how to schedule it reliably, how to avoid leaking credentials, and where WhatsApp Business (via Twilio) imposes limits you cannot ignore. And at the end, an honest section on what did not work.
Why This Project?
Staying updated with the latest exchange rates is crucial for traders, investors, and anyone dealing with multiple currencies. However, constantly checking the rates takes time and effort. This project automates the entire process, ensuring you receive real-time updates directly on your WhatsApp, making it easier to make informed decisions.
I usually have to be on top of things while busy. So sending me these conversions and values via WhatsApp helps me stay on top of my swing trade and know when to buy or sell a currency. This saves me from having to remember and keep logging into coin sites.
A pragmatic caveat: a notification bot is not investment advice and does not replace a serious market terminal. The goal here is the opposite of a screen full of charts - it is to reduce the number of times you need to open your phone to “just take a quick look.” One message twice a day with the numbers that matter already handles 90% of the anxiety of tracking currencies.
Features
- Automated Fetching: Retrieves current exchange rates for USD, BTC, EUR, and ETH.
- Conversion to BRL: Converts the fetched rates to Brazilian Real (BRL).
- WhatsApp Notifications: Sends the converted rates to your WhatsApp using the Twilio API.
- Environment Variables: Uses .env files to manage sensitive information securely.
Choosing the Rate Source
This is the most important decision in the project, and the one people think about least before starting. The data source determines whether the bot is reliable or whether it breaks silently on some random Tuesday.
There are three common paths:
1. Scraping web pages (Google Finance, currency sites). The easiest to start with and the most fragile to maintain. You use requests + BeautifulSoup to read the HTML and extract the number. It works until the site changes a CSS class or adds a bot block, and then your bot stops without warning. There is no API contract, so no stability guarantee. Use it only for a prototype.
2. Market libraries (yfinance). yfinance pulls data from Yahoo Finance and covers stocks, FX (the USDBRL=X pair) and crypto. It is free and convenient, but it is not an official API: it also relies on internal Yahoo endpoints that change without notice. Great for personal use, risky for anything that needs an SLA.
3. Dedicated exchange-rate APIs. Services like exchangerate.host, Open Exchange Rates, Fixer, CurrencyAPI or Frankfurter (which serves European Central Bank data, free and keyless). These have a stable JSON response contract, documentation, and free tiers with request limits. For crypto, CoinGecko has a generous free plan.
My advice, after getting burned by scraping: start with a real JSON API. The free tier of most of them (a few hundred to a few thousand requests per month) is plenty for a bot that runs twice a day, which is about 60 requests a month. An example read against a JSON API:
import requests
def get_usd_brl() -> float:
resp = requests.get(
"https://api.frankfurter.app/latest",
params={"from": "USD", "to": "BRL"},
timeout=10,
)
resp.raise_for_status()
return resp.json()["rates"]["BRL"]
Fact-check note: plan names, request limits and whether each provider is free change often. Confirm the current rate limit and cost in the provider’s documentation before committing to a choice.
How It Works
The flow is simple and worth understanding before touching the code: (1) the script calls the exchange-rate API and receives the values; (2) it builds a readable text message with the rates and the date/time; (3) it uses the Twilio API to send that message on WhatsApp; (4) a scheduler triggers the script at the times you define. Each step can fail differently, so I handle errors at each one.
Originally the project used requests, BeautifulSoup and yfinance to fetch and parse rates from Google Finance. If you follow the advice in the previous section and swap in a JSON API, the rest of the flow stays identical. Here is a step-by-step guide to set it up.
Step 1: Clone the Repository
First, clone the GitHub repository to your local machine:
git clone https://github.com/yourusername/currency-exchange-whatsapp-notifier.git
cd currency-exchange-whatsapp-notifier
Step 2: Set Up the Environment
Create a virtual environment and activate it:
python3 -m venv venv
source venv/bin/activate # On Windows use: venv\Scripts\activate
Install the required dependencies:
pip install -r requirements.txt
Step 3: Configure Environment Variables
Create a .env file in the project root and add your Twilio credentials and WhatsApp numbers:
TWILIO_ACCOUNT_SID=your_twilio_account_sid
TWILIO_AUTH_TOKEN=your_twilio_auth_token
TWILIO_WHATSAPP_NUMBER=whatsapp:+14155238886
RECIPIENT_WHATSAPP_NUMBER=whatsapp:+recipient_phone_number
Credential Security: Don’t Commit Your Tokens
This deserves its own section because it is the most common mistake and the most expensive one. The TWILIO_AUTH_TOKEN is effectively a password for your account: anyone who has it can send messages and spend your balance. Three rules I always follow:
1. .env never goes into Git. Add the line to .gitignore before your first commit, not after:
# .gitignore
.env
venv/
__pycache__/
2. Load from the environment, never hardcode. In code, read the variables and fail fast if they are missing, instead of passing a None to Twilio and getting an obscure error:
import os
from dotenv import load_dotenv
load_dotenv()
def require_env(name: str) -> str:
value = os.environ.get(name)
if not value:
raise RuntimeError(f"Missing required environment variable: {name}")
return value
ACCOUNT_SID = require_env("TWILIO_ACCOUNT_SID")
AUTH_TOKEN = require_env("TWILIO_AUTH_TOKEN")
3. If it leaks, rotate it. If a token lands in a public commit, deleting the file is not enough - the Git history keeps it. Generate a new token in the Twilio console and revoke the old one immediately. In production, prefer a secrets manager (your CI runner’s environment variables, AWS Secrets Manager, etc.) over a .env on disk.
Step 4: Build the Message and Send via Twilio
With the rates in hand, build a readable text and send it. The Twilio Python client pattern is straightforward:
from twilio.rest import Client
def build_message(rates: dict[str, float]) -> str:
lines = ["Today's rates (BRL):"]
for currency, value in rates.items():
lines.append(f"- {currency}: R$ {value:,.2f}")
return "\n".join(lines)
def send_whatsapp(body: str) -> str:
client = Client(ACCOUNT_SID, AUTH_TOKEN)
msg = client.messages.create(
from_=require_env("TWILIO_WHATSAPP_NUMBER"),
to=require_env("RECIPIENT_WHATSAPP_NUMBER"),
body=body,
)
return msg.sid
Note that the number needs the whatsapp: prefix (e.g. whatsapp:+5511999999999), otherwise Twilio treats it as SMS. To test without production cost, Twilio offers a WhatsApp sandbox: you send a keyword to their sandbox number and can then exchange messages for 72 hours, enough to validate the flow before setting up an approved number.
Error Handling and Retries
A scheduled bot needs to survive a network hiccup without dying. The two external calls - the exchange-rate API and Twilio - can time out, return a 5xx, or hit a rate limit. I wrap each one with retry and backoff:
import time
import requests
def with_retry(fn, attempts=3, wait=5):
for i in range(attempts):
try:
return fn()
except (requests.RequestException, Exception) as e:
if i == attempts - 1:
raise
print(f"Attempt {i + 1} failed: {e}. Retrying in {wait}s.")
time.sleep(wait)
Good practices worth the effort: always set a timeout on requests (without it the script can hang indefinitely); handle the case where the API returns JSON without the expected field; and, above all, make the error visible. A bot that fails silently is worse than no bot, because you trust a message that never arrives. Log to a file and, if possible, send a failure notification (a second channel, a simple email) when the job exhausts all its retries.
Step 5: Schedule the Script
You can schedule the script to run at specific times. There are four approaches, from simplest to most robust:
cron (macOS and Linux). The classic path for an always-on machine. Edit the crontab:
crontab -e
And add the lines (here, 8 AM and 2 PM daily):
0 8 * * * /path/to/venv/bin/python /path/to/send_exchange_rates.py >> /path/to/bot.log 2>&1
0 14 * * * /path/to/venv/bin/python /path/to/send_exchange_rates.py >> /path/to/bot.log 2>&1
Always use absolute paths (to the venv’s Python and to the script) and redirect output to a log - cron does not run with the same PATH as your interactive shell, and that is where a lot of people get stuck.
APScheduler (inside Python itself). If you want the Python process to stay running and handle scheduling on its own, without depending on the system cron, the APScheduler library solves it. Useful when you already have a long-running process or plan to package it in a container.
GitHub Actions (serverless). My favorite for personal bots: no machine needs to stay on. A workflow with schedule runs on a cron time on GitHub runners, for free within the minutes quota of public repositories or within the free tier. Credentials live in GitHub Secrets, not on disk. Outline:
# .github/workflows/rates.yml
on:
schedule:
- cron: "0 11,17 * * *" # UTC; equals 8 AM and 2 PM in BRT
jobs:
send:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install -r requirements.txt
- run: python send_exchange_rates.py
env:
TWILIO_ACCOUNT_SID: ${{ secrets.TWILIO_ACCOUNT_SID }}
TWILIO_AUTH_TOKEN: ${{ secrets.TWILIO_AUTH_TOKEN }}
Watch two GitHub Actions details: the cron is always in UTC (subtract 3 hours from Brasília time), and scheduled jobs can be delayed during peak times - if you need minute-level precision, this is not the right tool.
Managed cloud scheduler. In the cloud, AWS EventBridge triggering a Lambda, or GCP’s Cloud Scheduler, does the same with stronger delivery guarantees. Overkill for personal use, but it is the natural path if the bot becomes something serious.
Twilio Cost and WhatsApp Limits
Here is the part that turns the project from “toy” into “something you need to understand before scaling.” WhatsApp Business, which Twilio resells, is not free-form SMS - it has its own rules:
-
The 24-hour window. You can only send free-form text to a user within 24 hours of the last message they sent you. Outside that window, you must use a message template pre-approved by WhatsApp. Since a rate bot sends on its own initiative (the user is not replying to anything), in practice you land in the approved-template case for “real” use, outside the sandbox.
-
Templates need approval. Registering a template (“Today’s rates: {{1}}”) goes through WhatsApp review and takes from minutes to a few days. It is a bureaucratic step that catches people who only tested in the sandbox off guard.
-
Billing per conversation/message. Twilio charges for WhatsApp usage, and WhatsApp’s model is based on initiated conversations, with pricing that varies by country and by category (marketing, utility, authentication). Add Twilio’s own per-message fee on top. For a personal bot sending 2 messages a day, the monthly cost tends to be low, but it is not zero outside the sandbox.
Fact-check note: exact values (price per conversation, Twilio’s fee, the exact window duration and template approval times) change and vary by country. Confirm in the official Twilio and WhatsApp Business documentation before assuming any number. In the sandbox, test cost is waived, but the 72-hour session window and recipient restrictions apply.
What Did NOT Work / Limits
Being honest about where I hit my head:
- Google Finance scraping broke. The initial version relied on reading HTML, and one layout change was enough to make the parser return
Nonewith no clear error. That is what pushed me toward JSON APIs. If you inherited a version withBeautifulSoup, treat that as technical debt. yfinancedisappears without warning. On some daysyfinancereturned empty due to rate limiting or Yahoo endpoint instability. It works 95% of the time, but the other 5% teach you to handle the “no data” case instead of sending a message with a wrong value.- The 24-hour window caught me. Everything worked in the sandbox, and it only clicked when I tried to leave it: proactive notification requires an approved template. If you do not plan for that, the bot “works in testing” and fails in production.
- cron on a laptop is an illusion of reliability. While it ran on my Mac, every sleep or reboot lost triggers. Migrating to GitHub Actions solved the “machine has to be on.”
- Without a failure alert, you don’t know it broke. I spent days thinking I was receiving rates when the job was silently failing. Error visibility is not optional.
How to Extend It
Once the basics work, you can evolve without a rewrite:
- Multiple currencies and pairs. Parameterize the list of pairs (
USD/BRL,EUR/BRL,BTC/BRL) and iterate. The message builder already handles a dictionary. - Threshold alerts. Instead of sending every time, only notify when the dollar crosses a value you care about (e.g. below R$ 5.00). This reduces messages and solves the WhatsApp cost/window as a bonus.
- History and chart. Save each read into a CSV or SQLite and generate a simple chart with
matplotlibto attach a weekly trend. - Percentage change. Store the last rate and send “USD: R$ 5.42 (+0.8% vs yesterday)” - the delta is usually more useful than the absolute number.
Conclusion
With the Currency Exchange Rate to BRL (or other country/currency) WhatsApp Notifier, you can effortlessly stay updated with real-time exchange rates. Whether you are a trader, investor, or someone who needs to keep track of currency conversions, this tool can be a valuable addition to your workflow. Just do not skip the three decisions that separate the toy from the reliable bot: a stable data source, a scheduler that does not depend on your machine being on, and understanding the WhatsApp Business rules before leaving the sandbox.
Check out the GitHub repository for the complete source code and detailed instructions.
If you found this project helpful, please give it a star on GitHub and share it with others who might benefit. Your feedback and contributions are always welcome.
Further Reading
- Twilio Docs for WhatsApp - official setup, sandbox and templates guide.
- WhatsApp Business messaging policy and rules - the 24-hour window and conversation categories.
- Frankfurter API - free, keyless exchange-rate API based on ECB data.
- APScheduler documentation - scheduling inside Python itself.
Originally published on Medium
This article was originally published on June 15, 2024. The version on buildcomcarlos.com is the integral editorial copy maintained on my site. You can read the original on Medium with the original layout, claps and responses.
Enjoyed the article?
Like it and leave a comment. It helps me decide what to write next.
Comments