On 409 in particular
409 also covers the correct-behaviour cases: a marketing template
addressed to somebody who opted out is refused. That is not a bug to work
around.
Documentation Index
Fetch the complete documentation index at: /llms.txt
Use this file to discover all available pages before exploring further.
What each status means, and whether retrying will help.
| Status | Means | What to do |
|---|---|---|
400 | Malformed request, or a template blank is missing | Fix and resend. Retrying unchanged fails identically |
401 | Key missing, wrong or revoked | Check the header. Do not retry |
404 | No template of that name on your account | Check the name. Names are per account, so another business’s template is invisible to you |
409 | Template not approved, WhatsApp not connected, contact opted out, or Meta has restricted the account | Look at Templates or Channels. Retrying makes a restriction worse |
409 | The same idempotency key is still in flight | Retry in a moment; the first request will have answered by then |
429 | Rate limited | Wait for Retry-After |
503 | WhatsApp is temporarily unavailable | Ours to fix, not yours. Safe to retry with the same idempotency key |
409 also covers the correct-behaviour cases: a marketing template
addressed to somebody who opted out is refused. That is not a bug to work
around.
const RETRYABLE = new Set([429, 500, 502, 503, 504]);
async function send(payload, key, attempt = 0) {
const res = await post(payload, key);
if (res.ok) return res.json();
if (!RETRYABLE.has(res.status) || attempt >= 4) {
// 400, 401, 404 and 409 never succeed on a retry. Surface them.
throw new Error(`${res.status}: ${await res.text()}`);
}
const after = Number(res.headers.get("Retry-After") ?? 0);
const backoff = after > 0 ? after * 1000 : 2 ** attempt * 1000;
await sleep(backoff);
// Same idempotency key on every attempt — that is what makes this safe.
return send(payload, key, attempt + 1);
}