Sending messages
HookChat has exactly two send routes: an in-window reply and an out-of-window human-agent send. There is no generic send, and there are no message tags. Meta Message Tags ended on 10 February 2026; the human-agent route is the only way to reach someone outside the 24-hour window.
The messaging window model
Meta only lets a business message a person inside time windows measured from that person's last inbound message. HookChat computes the window for every conversation server-side and surfaces it: you never recompute Meta's rules. There are three states:
open_24h: within 24 hours of the last inbound message. Any reply is allowed.human_agent_only: from 24 hours to 7 days. Only a human-typed message via the human-agent route is allowed.closed: past 7 days (or no inbound message yet). Nothing can be sent. On Instagram in Australia there is no route past 7 days at all.
Every conversation carries the computed window and two boolean flags, so your UI never has to guess:
"window": { "state": "open_24h", "expires_at": "2026-08-29T03:14:22Z" },
"can_reply": true,
"can_send_as_human_agent": truecan_reply: true only while the window isopen_24h.can_send_as_human_agent: true while the window is not closed (open_24horhuman_agent_only).
window.expires_at is a snake_case ISO-8601 timestamp, like every other timestamp in the API. It is absent when state is closed: a closed window has no expiry.Targeting: conversation_id only
Both send routes target a single conversation by its conversation_id. That id, and the key you authenticate with, is the whole addressing scheme. There is deliberately no ?tenant parameter on the send routes: the conversation id already scopes the send to one tenant's conversation.
Reply in-window POST /v1/messages/reply
Sends a reply on a conversation whose 24-hour window is open. text is required unless attachments is present: a media-only reply is valid. Out of the 24-hour window this route refuses with window_closed (409); use the human-agent route instead.
POST /v1/messages/reply
Authorization: Bearer hookchat_live_…
Content-Type: application/json
{
"conversation_id": "CONV#acme#instagram#17841405309211844#6021573449812077",
"text": "Happy to help, can you send a reference image?"
}On success:
{ "ok": true, "data": { "platform_message_id": "mid.def456" } }Human-agent send POST /v1/messages/human-agent
The only route out of the 24-hour window (from 24 hours to 7 days), and it is human-sent by definition. actor_id is required: the human who sent the message must be named. Omitting it is a 409 missing_actor policy refusal, not a 400. Past 7 days the window is human_agent_unavailable (409).
POST /v1/messages/human-agent
Authorization: Bearer hookchat_live_…
Content-Type: application/json
{
"conversation_id": "CONV#acme#instagram#17841405309211844#6021573449812077",
"text": "Sorry for the delay, following up personally.",
"actor_id": "console:01H…"
}The success shape is identical to the reply route: { ok: true, data: { platform_message_id } }.
Attachments and reply_to
Both routes accept an optional attachments array ({ type, url }: Meta pulls the asset from the URL) and an optional reply_to (a platform message id). Attachments spend the media rate budget; reply_to is a documented no-op where the platform doesn't support replies. Neither changes the window, rate, or policy decision.
Errors
Failures use the standard envelope: { ok: false, error: { code, message, detail? } }. Branch on error.code: a stable token from the closed vocabulary, never on error.message. A 409 means the request was well-formed and the thread's state refused it (retrying won't help). A 502 send_failed is the one exception: policy allowed the send and the delivery itself died, so a retry can help.
| code | HTTP | Route | Meaning |
|---|---|---|---|
invalid_request | 400 | both | Malformed body or an absent required field: e.g. no conversation_id, or text missing on a send with no attachments. |
unauthorized | 401 | both | Missing or invalid bearer API key. |
conversation_not_found | 404 | both | No such conversation. A cross-tenant id is a 404, never a 403: ids do not leak across tenants. |
window_closed | 409 | reply | The 24-hour reply window is closed. This also covers the human-agent-only state on the reply route; the conversation’s can_send_as_human_agent flag tells you whether to offer that path. |
human_agent_unavailable | 409 | human-agent | The conversation is past the 7-day human-agent window. Nothing exists past 7 days (Meta policy). |
missing_actor | 409 | human-agent | A human-agent send omitted actor_id. The human who sent it must be named; this is a policy refusal, not a malformed request. |
rate_limited | 409 | both | The account’s send budget for this category is exhausted. |
send_failed | 502 | both | Policy allowed the send and the delivery itself failed (Meta refused / network). The one non-policy failure: a retry can help, where retrying a 409 cannot. detail carries Meta’s message. |
These are the only codes the send routes return. The full API-wide vocabulary (including forbidden, tenant_required, the *_not_found family, and replay_conflict) is a closed set of 17 codes; see Webhooks and events for the delivery-side codes.