πŸš€ GoSendAPI is in beta β€” Meta App Review in progress. Get on the waitlist β†’
Core ConceptsConversations

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:

CategoryInitiated byUse caseCost
ServiceUser (inbound)Customer support replyFree (after Nov 2024)
UtilityBusiness via templateOrder confirm, reminder, OTPLow
AuthenticationBusiness via templateOTP, login codesLowest
MarketingBusiness via templatePromo, re-engagementHighest

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)
StatusMeaning
activeInside 24h window, both parties exchanging
inactiveNo activity for X minutes (still inside 24h)
ended24h elapsed β€” only templates allowed

Properties

FieldTypeDescription
idbigintInternal ID
tenant_idbigintProject that owns the conversation
customer_idbigintOptional β€” for joining to your Customer model
phone_number_idbigintWhich line is talking
contact_phonestringThe user’s phone (E164)
contact_bsuidstringMeta’s stable user ID (use for analytics)
contact_namestringWhatsApp profile name (if user shared it)
statusenumactive / inactive / ended
ended_reasonstringWhy it ended (timeout/manual_close/agent_close)
messages_countintTotal messages in this conversation
last_inbound_atdatetimeLast user message
last_outbound_atdatetimeLast business message
started_atdatetimeWhen the conversation opened
ended_atdatetimeWhen 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 range

Closing 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

EventWhen
whatsapp.conversation.createdFirst inbound message from a contact (new conversation opens)
whatsapp.conversation.inactiveNo activity for N minutes (still inside 24h)
whatsapp.conversation.ended24h 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 allowed

Avoiding 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)
}

What’s next