# The 24-hour window

> When you can send free-form WhatsApp text, when you must use a template, and the 409 you get when you are wrong.

Source: https://docs.amomic.in/channels/whatsapp-window

---

Everything that goes wrong on WhatsApp goes wrong here. If you read one page about this channel, read this one.

WhatsApp does not let a business message whoever it wants whenever it wants. You may write freely to a customer only in the 24 hours after *they* last wrote to you. Outside that window you may send only messages Meta has already reviewed and approved — templates. There is no third option.

Amomic-AI enforces this centrally, on the send path, so no caller can route around it: not the API, not an agent, not the console.

## The window in one picture

```mock
{"component": "WindowDiagram", "props": {"caption": "The window opens on the customer's message and closes 24 hours later. Your replies do not extend it — only theirs do."}}
```

## What opens it

**A message from the customer. Only that.**

Every inbound message from that customer to that sender restarts the 24-hour clock from the moment it arrives. A customer who writes to you every day keeps the window permanently open. A customer who wrote once last Tuesday is long outside it.

Things that do **not** open or extend the window:

- Your agent replying. Outbound never extends the clock.
- Sending an approved template. It reaches them, but it does not open a free-form window on its own.
- A delivery or read receipt.
- The customer opening the chat, or the message being marked read.

A customer's message does **not** open the window when Amomic-AI doesn't take it in:

- the workspace has used its plan's conversation allowance and this would be a new conversation (the message is saved, but no window opens);
- the plan no longer includes WhatsApp and the number is in its seven-day hold;
- the number is **Off** with **Don't keep these conversations** on (the message is dropped on arrival).

In those cases a free-form reply from your team still returns `409`.

The window is scoped **per customer, per sender**. If a customer messages one of your numbers, you have an open window on that number and no window on the others.

## What you can send, when

| Window | Free-form `text` | Approved `template` |
|---|---|---|
| **Open** — they messaged within 24 hours | Yes | Yes |
| **Closed** — they have not, or never did | No — `409` | Yes |

Templates deliberately bypass the window check. That is the entire reason they exist: they are the sanctioned way to start a conversation with someone who is not currently talking to you.

::: warning What templates do not bypass
Opt-outs. A suppressed recipient rejects templates exactly as hard as free text — `403 permission_denied` with `details.reason` of `"suppressed"`. See [opt-outs](/channels/whatsapp#opt-outs).
:::

## The error, exactly

Send free-form text to a closed window and you get this, every time:

```responses
[
  {"status": 409, "code": "conflict", "reason": "session_window_closed", "description": "The 24-hour session window is closed; only approved template messages can be sent."}
]
```

```json title="409 Conflict"
{
  "error": {
    "code": "conflict",
    "message": "The 24-hour session window is closed; only approved template messages can be sent.",
    "details": { "reason": "session_window_closed" },
    "request_id": "req_01JQRS4T8XZ0AB1CD2EF3GH4"
  }
}
```

Branch on `error.details.reason`, not on the message string. `conflict` covers more than one situation across the API; `session_window_closed` is unambiguous.

```javascript title="Handling it properly"
const response = await fetch("https://services.amomic.in/api/v1/messages", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.AMOMIC_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": `order-${order.id}-shipped`,
  },
  body: JSON.stringify({ to: order.customerPhone, text: message }),
});

if (response.status === 409) {
  const { error } = await response.json();
  if (error.details?.reason === "session_window_closed") {
    // Not retryable as free text. Fall back to the approved template.
    return sendTemplate(order);
  }
}
```

::: danger A 409 here is not transient
Retrying the same free-form send will fail identically until the customer writes to you again — which may be never. Backoff does not help. Queueing does not help. The only correct handling is to send a template instead, or to not send at all.
:::

## Nothing is sent automatically when the window closes

For a message you send through [`POST /messages`](/api/messages), there is no hold-and-release, no automatic re-engagement template, and no queue that drains when the customer next writes in.

When the window closes, the closed window is simply the state of the world. If you have something to say, you send a template. If you had a message pending in your own system, it is your system's job to notice the 409 and decide what happens next.

We are explicit about this because the alternative — quietly substituting a template you did not choose, on a Meta conversation category you did not budget for, to a recipient who may not want it — is worse than an error.

::: note Ticket replies are the one exception, and they ask first
A staff reply typed on a [ticket](/console/tickets) *is* held and released, because there the three objections above do not apply: the template is one you approved for this purpose, its conversation category is UTILITY, and nothing reaches the customer until **they** tap to ask for it.

The sequence is: the window is closed, so the customer receives a short notification template instead of the reply; tapping **View update** re-opens the window as a genuine inbound message; the held reply — and every other one waiting on that ticket, in the order it was written — is then delivered.

This never happens on the API path, and it never happens without a template your workspace has submitted and Meta has approved. See [Working tickets](/console/tickets).
:::

## The consequence for your design

Work through the timing of a real notification and the conclusion is unavoidable.

A customer orders at 11 a.m. and asks a question in chat; the window opens. The order ships at 9 p.m. the next day — 34 hours later. The window closed 10 hours ago. Your "your order has shipped" message is a template, or it does not go out.

Now generalise. **Anything triggered by your system rather than by the customer is almost always outside the window**, because your systems run on your schedule and customers write on theirs:

| Flow | Triggered by | Realistically |
|---|---|---|
| Answering a support question | The customer, just now | Inside the window |
| Agent follow-up during a live chat | The customer, minutes ago | Inside the window |
| Order shipped / out for delivery | Your warehouse | Outside |
| Appointment reminder for tomorrow | A scheduler | Outside |
| Payment receipt, renewal notice | Your billing job | Outside |
| Feedback request a day later | A cron | Outside, by definition |
| Re-engaging a lapsed customer | A report | Far outside |

So: **build every notification flow on templates from the start.** Not as a fallback bolted on after the first 409 in production — as the primary path.

The pattern that survives contact with reality:

::: steps

::: step Get the template approved before you write the code

Template review takes minutes to a day. Doing it after your integration is built means your launch waits on Meta. See [Templates](/channels/whatsapp-templates).
:::

::: step Send the template unconditionally for anything system-triggered

Do not probe the window first. There is no endpoint that reports window state, and a template send works whether the window is open or closed. Checking would only add a failure mode.
:::

::: step Use free-form text only inside a conversation you are already in

An agent replying to a live conversation is inside the window by construction — the customer's message is what woke it up. That is where free text belongs.
:::

::: step Treat 409 / `session_window_closed` as "switch to a template", not as "retry later"

One code path, one decision, no queue.
:::

:::

## What the agent does when it hits a closed window

An agent can be given the ability to send a WhatsApp message — following up a web chat, for example. When it tries and the window is closed, it does not silently invent a substitute or apologise vaguely. The tool returns a structured result the model can act on, using these exact values:

`sent` · `template_required` · `needs_setup` · `opted_out` · `template_not_allowed` · `template_not_found` · `template_unusable` · `needs_information` · `not_configured` · `failed`

`template_unusable` means the template needs an image or video that hasn't been attached in the console, so the agent is steered to another allowed template.

`template_required` means the window is closed and the agent should reach for one of the templates it is permitted to send. Which templates those are is set on the agent's tool policy — the model cannot name a template you have not allowed. See [Tools & actions](/build/tools).

## Verifying the window is doing what you think

The **Verify a conversation** card on a number's **Overview** tab, under **Channels → WhatsApp**, is a window test in disguise. It sends a free-form reply, so it only works inside the window.

1. Message your business number from your phone. The window opens.
2. Enter that number in **Customer phone number** and choose **Send verification reply**.
3. *"Your Amomic WhatsApp connection is working."* arrives on your phone.

Skip step 1 and the send fails with the same error the API returns: *"The 24-hour session window is closed; only approved template messages can be sent."* That is the window rejecting a free-form send, end to end, in ten seconds.

![The WhatsApp Overview tab with a number typed into Verify a conversation and a red toast reading The 24-hour session window is closed; only approved template messages can be sent](https://docs.amomic.in/screens/whatsapp/verify-window-closed.light.webp)

*The verification reply refused because the customer hasn't written in the last 24 hours.*

The same card can also fail with *"Recipient has opted out."* if that phone number sent `STOP` to any of your numbers. Send `START` from it first.

## Common misreadings

::: details "My template send opened the window, so now I can send text"
It did not. A template reaches the customer; only a message *from* the customer opens the window. If they reply to your template, that reply opens it.
:::

::: details "The customer read my message, so the window is open"
Read receipts are not inbound messages. Nothing about the customer's behaviour inside WhatsApp opens the window except sending you something.
:::

::: details "It worked in testing and fails in production"
In testing you were messaging the number yourself minutes earlier, so every window was open. Production traffic is triggered hours later by your own systems. This is the single most common way this rule is discovered.
:::

::: details "We have a second number, so we can message from that one"
The window is per sender, so the second number's window is also closed — you have gained nothing except a second closed window. Opt-outs, meanwhile, *are* shared across senders, so the second number does not help there either.
:::

::: details "Can I query whether the window is open?"
No. There is no endpoint that reports window state. Send the template.
:::

## Next

::: cards

::: card Templates
[How to write and submit one](/channels/whatsapp-templates), what Meta approves, and how to fill its variables at send time.
:::

::: card POST /messages
[The send contract](/api/messages) — text, templates, sender selection, and every error code.
:::

::: card Order-shipped update
[A complete flow](/recipes/order-updates) built the right way round, template first.
:::

::: card Errors
[The full error model](/api/errors), including how `code` and `details.reason` divide up.
:::

:::
