· 5 min · TextMeFlow Team

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