Conversations
A conversation is a 24h window starting from the first inbound message (or from a template-initiated outbound). Inside the window you can send anything freely; outside, you can only send approved templates.
This is the single most important concept for WhatsApp pricing and API mental model.
The 24h rule
User sends a message βββ
β
βΌ
βββββββββββββ
β 24h window β βββ you can send anything
βββββββββββββ
β
βΌ
window expires
β
βΌ
(only templates from here)If your customer hasnβt messaged you in 24 hours:
- You cannot send a free-form text β Meta returns
403 Forbidden - You must send a pre-approved template
- Sending a template starts a new conversation window
Conversation categories (Meta pricing)
Meta charges per conversation, not per message. Pricing depends on who initiated and the category:
| Category | Initiated by | Use case | Cost |
|---|---|---|---|
| Service | User (inbound) | Customer support reply | Free (after Nov 2024) |
| Utility | Business via template | Order confirm, reminder, OTP | Low |
| Authentication | Business via template | OTP, login codes | Lowest |
| Marketing | Business via template | Promo, re-engagement | Highest |
Since November 2024, Service conversations (user-initiated support) are free. Meta still charges for utility/marketing/auth initiated by business. Pricing varies per country.
Conversation lifecycle
none ββ> active (within 24h of last activity)
β
βββ> inactive (after N minutes of silence, configurable β typically 30min)
β
βΌ
ended (24h elapsed since last business or user message)| Status | Meaning |
|---|---|
active | Inside 24h window, both parties exchanging |
inactive | No activity for X minutes (still inside 24h) |
ended | 24h elapsed β only templates allowed |
Properties
| Field | Type | Description |
|---|---|---|
id | bigint | Internal ID |
tenant_id | bigint | Project that owns the conversation |
customer_id | bigint | Optional β for joining to your Customer model |
phone_number_id | bigint | Which line is talking |
contact_phone | string | The userβs phone (E164) |
contact_bsuid | string | Metaβs stable user ID (use for analytics) |
contact_name | string | WhatsApp profile name (if user shared it) |
status | enum | active / inactive / ended |
ended_reason | string | Why it ended (timeout/manual_close/agent_close) |
messages_count | int | Total messages in this conversation |
last_inbound_at | datetime | Last user message |
last_outbound_at | datetime | Last business message |
started_at | datetime | When the conversation opened |
ended_at | datetime | When it closed (null if active) |
Listing conversations
GET /v1/conversations?status=active&phone_number_id=555123456789&page=1
# Filters:
# status = active | inactive | ended
# customer_id = filter to one customer
# contact_phone = filter to one contact
# since/until = date rangeClosing a conversation manually
If your operator finishes a support interaction, you can mark the conversation as ended so Meta bills it sooner and your dashboards reflect the close:
POST /v1/conversations/42/close
{
"reason": "agent_close"
}Reasons: manual_close, agent_close. Optional.
Closing a conversation does NOT prevent the user from sending more messages β they can still reach you. It only marks the conversation as closed for billing/reporting purposes.
Webhooks
| Event | When |
|---|---|
whatsapp.conversation.created | First inbound message from a contact (new conversation opens) |
whatsapp.conversation.inactive | No activity for N minutes (still inside 24h) |
whatsapp.conversation.ended | 24h window closed |
These events let you trigger workflows (assign agent, send follow-up template, etc.).
Common patterns
βAre we inside the 24h window for user X?β
Check the most recent active conversation for that contact:
const res = await fetch(
`https://cloud.gosendapi.com/v1/conversations?contact_phone=5491140123456&status=active`,
{ headers: { 'X-API-Key': process.env.GOSENDAPI_KEY } }
);
const { data } = await res.json();
const canFreeText = data.length > 0;If canFreeText β send any message type. If not β must use a template.
Re-engaging a contact who went silent
1. User has been silent for >24h
2. You want to re-engage with "Hi, did you forget?"
3. β Can't send free text
4. β Send approved MARKETING template "re_engagement_v1"
5. If user replies β 24h window opens, free-text again allowedAvoiding accidental marketing send during active conversation
// Bad: always send template
await sendTemplate(phone, 're_engagement_v1', vars);
// Good: check conversation status first
const conv = await getActiveConversation(phone);
if (conv) {
await sendText(phone, 'Hi, just following up!'); // free
} else {
await sendTemplate(phone, 're_engagement_v1', vars); // template (paid)
}