Send WhatsApp Messages from AWS Lambda (Node.js)
If your backend already runs on AWS — an API Gateway endpoint, an S3 upload trigger, a DynamoDB stream, a scheduled EventBridge rule — adding a WhatsApp notification shouldn't mean spinning up a new server just to call an HTTP API. A Lambda function can send the message itself, with no process to keep alive and no extra infrastructure to pay for between invocations.
This guide covers a Lambda triggered by API Gateway (a webhook receiver) and one triggered by EventBridge (a scheduled reminder) — the two most common patterns for transactional WhatsApp sends.
What you need
- A free TextMeFlow account with a WhatsApp number paired by QR code.
- Your API key from the TextMeFlow dashboard.
- A Lambda function using the Node.js 20.x (or later) runtime, which ships with the global
fetch— no extra HTTP library to bundle.
Step 1: store the API key as an environment variable
Never hardcode the key in the function body. In the Lambda console, go to Configuration → Environment variables and add TEXTMEFLOW_API_KEY. If you're deploying with the Serverless Framework, SAM, or CDK, pull it from AWS Secrets Manager or SSM Parameter Store at deploy time instead of committing it to your template.
Step 2: the send function
export const handler = async (event) => {
const body = JSON.parse(event.body ?? "{}");
const res = await fetch("https://api.textmeflow.eu/v1/messages", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.TEXTMEFLOW_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
to: body.phone,
text: body.message,
}),
});
if (!res.ok) {
const error = await res.json();
console.error("WhatsApp send failed", error);
return { statusCode: res.status, body: JSON.stringify(error) };
}
return { statusCode: 200, body: JSON.stringify({ sent: true }) };
};
This works unchanged whether the trigger is API Gateway (where event.body is the raw request body) or a direct Lambda invocation from another service — only the code that builds event.body changes.
Pattern A: webhook receiver (API Gateway trigger)
Wire the function behind an API Gateway HTTP API, and point an upstream webhook — Stripe, Shopify, a form submission handler, anything that can POST JSON — at that endpoint. The Lambda parses the payload, extracts the phone number and a message, and calls TextMeFlow. Because API Gateway already queues and retries failed deliveries to the Lambda, you don't need to build your own retry logic for the webhook hop itself — only for the TextMeFlow call, which the next section covers.
Pattern B: scheduled reminder (EventBridge trigger)
For reminders that fire on a schedule rather than an event — a daily batch of appointment reminders, for example — attach an EventBridge rule (rate(1 hour) or a cron expression) to the same function, reading from DynamoDB or RDS instead of event.body:
export const handler = async () => {
const dueReminders = await getDueReminders(); // your own query
const results = await Promise.allSettled(
dueReminders.map((r) =>
fetch("https://api.textmeflow.eu/v1/messages", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.TEXTMEFLOW_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ to: r.phone, text: r.text }),
})
)
);
return { statusCode: 200, body: JSON.stringify({ count: results.length }) };
};
Avoid Promise.all here — one rejected request shouldn't abort the whole batch. Promise.allSettled lets every reminder attempt independently, and you can log the rejected ones for a retry pass.
Handling throttling instead of fighting it
A batch of reminders fired from EventBridge is exactly the shape of traffic TextMeFlow's pacing logic is built to smooth out: consecutive sends to different recipients are spaced with a randomized delay rather than firing all at once, and a 429 response means you've hit your plan's hourly cap, not that something is broken. Check the Retry-After value in the response body and either queue the failed sends for a later Lambda invocation (an SQS dead-letter queue works well here) or let them wait for the next scheduled run. Don't loop and retry immediately inside the same invocation — that just trades one burst for another. The full set of pacing and rate-limit rules is in /docs/anti-spam.
Timeouts and cold starts
TextMeFlow's API typically responds well within a second, but set the Lambda's timeout to at least 10 seconds to leave room for cold starts and network variance — a 3-second default timeout will occasionally clip a slow invocation and report a false failure. If reminders run on a schedule, a cold start costs you nothing; if a webhook is on the critical path of a user-facing request, consider provisioned concurrency.
Scaling beyond the free plan
The free plan covers 50 messages a month for life, which is enough to wire up and test both patterns above. Once you're sending reminders or notifications at real volume, the Starter plan at €5/month raises your quota, with EU-hosted data (Paris) and an automatic DPA on paid plans. If you're also pulling incoming replies or delivery statuses into the same Lambda-based setup, see Send WhatsApp Messages from n8n, Make.com or Zapier for the no-code equivalent of the same webhook pattern.
Ready to wire it up? Create a free account and send your first WhatsApp message from a Lambda function in a few minutes.
Zelf WhatsApp-berichten versturen via API?
Gratis voor altijd tot 50 berichten/maand. QR scannen en binnen 5 minuten verstuur je je eerste bericht.
Gratis voor altijd