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:

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": true
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 } }.

There is no automated way to message someone outside the 24-hour window. The human-agent route exists precisely because a real person must stand behind an out-of-window message. This is a policy HookChat enforces, not a feature you can turn off.

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.

codeHTTPRouteMeaning
invalid_request400bothMalformed body or an absent required field: e.g. no conversation_id, or text missing on a send with no attachments.
unauthorized401bothMissing or invalid bearer API key.
conversation_not_found404bothNo such conversation. A cross-tenant id is a 404, never a 403: ids do not leak across tenants.
window_closed409replyThe 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_unavailable409human-agentThe conversation is past the 7-day human-agent window. Nothing exists past 7 days (Meta policy).
missing_actor409human-agentA human-agent send omitted actor_id. The human who sent it must be named; this is a policy refusal, not a malformed request.
rate_limited409bothThe account’s send budget for this category is exhausted.
send_failed502bothPolicy 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.