· 2 min · TextMeFlow Team

Handling rate limits and message status in the TextMeFlow API

Most WhatsApp-sending integrations work fine in testing and then start dropping messages in production the moment volume picks up. The usual cause isn't a broken API call — it's that nobody handled the 429 responses. TextMeFlow rate-limits by design (see the anti-spam guide for why), so your integration needs to expect it, not treat it as an error.

This guide covers the two things every production integration should do: back off correctly on 429, and poll message status instead of assuming a 202 means "delivered."

Why you'll see 429s

Every plan has an hourly ceiling and a per-minute burst limit — see the current numbers on the rate limits page. Two situations trigger a 429 from POST /v1/messages:

  • Burst exceeded — too many sends in the last minute. reason: burst_exceeded.
  • Hourly exceeded — you've hit the plan's hourly ceiling. reason: hourly_exceeded.

Both responses include Retry-After semantics in the body so you know exactly how long to wait — no guessing, no fixed sleep.

Retry with backoff, not a fixed delay

A naive retry loop (sleep(5); retry) either wastes time or hammers the API right when it's already telling you to slow down. Respect the retry hint and back off exponentially on top of it for safety margin.

Node.js:

async function sendMessage(to, text, attempt = 1) {
  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, text }),
  });

  if (res.status === 429 && attempt <= 5) {
    const body = await res.json();
    const wait = (body.retry_after_seconds ?? 30) * attempt;
    await new Promise((r) => setTimeout(r, wait * 1000));
    return sendMessage(to, text, attempt + 1);
  }

  return res.json();
}

Python:

import time
import requests

def send_message(to, text, attempt=1):
    res = requests.post(
        "https://api.textmeflow.eu/v1/messages",
        headers={"Authorization": f"Bearer {API_KEY}"},
        json={"to": to, "text": text},
    )
    if res.status_code == 429 and attempt <= 5:
        wait = res.json().get("retry_after_seconds", 30) * attempt
        time.sleep(wait)
        return send_message(to, text, attempt + 1)
    return res.json()

Cap the retries (five is plenty) and give up loudly after that — silently dropping a message is worse than logging a failure your team can act on.

202 means "queued," not "delivered"

POST /v1/messages returns 202 Accepted with a message_id the moment the message is queued. That's before it's even sent, let alone read — TextMeFlow's pacing engine may hold it for quiet hours or the random pacing delay described in the anti-spam guide.

If your app needs to know the real outcome — for a booking confirmation, a payment receipt, anything a human is waiting on — poll GET /v1/messages/{id} using the message_id you got back:

async function waitForStatus(messageId, maxTries = 10) {
  for (let i = 0; i < maxTries; i++) {
    const res = await fetch(`https://api.textmeflow.eu/v1/messages/${messageId}`, {
      headers: { Authorization: `Bearer ${process.env.TEXTMEFLOW_API_KEY}` },
    });
    const data = await res.json();
    if (data.status === 'delivered' || data.status === 'failed') return data;
    await new Promise((r) => setTimeout(r, 3000));
  }
  return null; // still pending after maxTries — don't block on it forever
}

For anything less time-sensitive, don't poll at all — configure a webhook and let TextMeFlow push status changes to you. Polling is for the one-off "did this specific message land" check; webhooks are for tracking everything at scale.

Don't fight the pacer with urgent

POST /v1/messages accepts an urgent: true flag that bypasses quiet-hours shaping. It's there for the door-code-at-11pm case, not as a way to skip the queue for convenience — every forced send during quiet hours adds to your account's risk score. Reserve it for messages a human is actively waiting on right now.

Putting it together

A resilient send path looks like this: send with retry-on-429, get back a message_id, either poll it for time-sensitive flows or let your webhook handler update your own database asynchronously, and reserve urgent for genuinely time-critical messages. That's the whole difference between an integration that silently loses messages under load and one that just works.

Start with the free plan — 50 messages/month, no credit card — and build the retry logic in before you need it, not after your first busy Saturday.

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