Chat Health
How we score the health of a chat and how it relates to your line's standing.
Review your setup with an agent. Copy this prompt into your own AI coding agent to check your integration against these guidelines.
You are auditing a codebase that integrates with the Linq Partner API
(iMessage / RCS / SMS messaging). Verify it follows Linq's best practices for
deliverability, chat health, and line reputation. This is READ-ONLY — do not
change code unless I ask.
Step 1 — Ground yourself in Linq's public docs. Start with the index at
https://docs.linqapp.com/llms.txt, then fetch the pages you need — at minimum
Best Practices, Chat Health, Phone Reputation, Sending Messages, and Webhooks,
plus the /v3 API reference for the endpoints below. (https://docs.linqapp.com/llms-full.txt
has every page in one file, but it is large — prefer the index and targeted
pages.) If you cannot fetch these, stop and tell me rather than auditing from
memory.
Step 2 — Locate the integration. Search the codebase for the Linq base URL
(api.linqapp.com/api/partner), "/v3/" request paths, an official SDK — Node
`@linqapp/sdk`, Python `linq-python` (imported as `linq`), or Go
`github.com/linq-team/linq-go` — and the inbound webhook handler, so you know
where sending, onboarding, and webhook handling live.
Step 3 — Audit against these requirements. Each item is something my code is
supposed to do — confirm whether it actually does, and cite the file and line:
Opt-out (compliance — most important)
- The code should scan every inbound message on the message.received webhook for
opt-out keywords — STOP, UNSUBSCRIBE, OPTOUT, CANCEL, END, QUIT (whole message;
exact and case-sensitive, except OPT OUT which matches in any casing, spaced,
hyphenated or not) — plus any clear "stop messaging me" intent, and a match
should immediately stop all outbound to that recipient. Linq rejects sends to
a keyword-opted-out recipient with 403 (error code 2024), but only the exact
keywords trigger that block — conversational stop requests are the code's job
to catch — and a 2024 rejection should be honored, not retried.
- Every send to an opted-out recipient is rejected, including a final courtesy
message. If the code sends one confirmation telling the recipient they can
reply any time to resume, that single request should set override_optout: true.
The override applies only to the request it is set on and does not lift the
block; each use is recorded, so it should appear once per opt-out, never in a
retry loop.
- The code should treat a chat whose health_status is OPTED_OUT as never-send
until Linq clears the status. Linq clears it as soon as the recipient replies
again in any chat with you (any inbound that is not itself an opt-out
keyword), so the code should gate on the current health_status rather than
tracking opt-ins itself.
Sending & line selection
- The code should send with POST /v3/messages using `to` and NO `from`. Linq
then picks the best line, load-balances across your pool, reuses the
recipient's existing healthy line, and fails over off a flagged line
automatically (see from_selection.reason in the response).
- The code should NOT call GET /v3/available_number (or pin a fixed `from`)
before each send — that defeats the automatic load-balancing and failover.
Onboarding new users
- The code should use GET /v3/available_number when onboarding a NEW user, to
get the best available line (and its vcf_url contact card) to show them — e.g.
a number or deeplink shown at signup — so new users spread evenly across the
pool. That is what available_number is for; it is not a per-message call.
Contact card
- The code should create the contact card once per line with
POST /v3/contact_card (initial setup only — later changes use
PATCH /v3/contact_card), and share it through the dedicated
POST /v3/chats/{chatId}/share_contact_card endpoint.
- New contacts should be inbound-first — let the recipient message first. The
card should be shared only after at least one outbound message exists in the
chat, and re-shared about once a day, since there's no confirmation the user
saved it.
Health & reputation gating
- Before sending, the code should check the chat's health_status and the line's
reputation from GET /v3/phone_numbers, and slow or pause on AT_RISK /
CRITICAL. It should also handle the phone_number.status_updated webhook to
react when a line's reputation changes.
- New users should onboard onto HEALTHY lines. The code should NOT migrate users
off an AT_RISK line to escape the status — improve engagement and let the line
recover instead.
Engagement & cadence
- Outbound should be built to get replies (aim for 3+ replies early and roughly
a 1:2 inbound:outbound ratio). When a recipient stops replying, the code
should slow down and then stop, rather than keep messaging someone who isn't
responding.
Volume & ramp
- The code should keep each line under ~7,000 messages/day (inbound + outbound).
That is a performance guideline, not a reputation threshold — steady high
volume with healthy reply rates is fine.
- The code should not start roughly 50 or more brand-new conversations per line
in a rolling 24 hours. Check bulk import, list upload, and campaign kickoff
paths for anything that opens a whole audience at once, and confirm first
contact is spread across days and across lines.
- The code should ramp a line's daily volume gradually rather than jumping
several-fold above what that line has recently been sending. Look for
scheduled or triggered sends that can take a quiet line to a large day in one
step.
Step 4 — Report:
1. A table: Check | Status (pass / gap / n/a / unknown) | Where (file:line) | Fix.
2. A short action list, highest deliverability and compliance risk first.
Ground every finding in code you actually read. If you cannot determine an item,
mark it unknown rather than guessing. Every chat carries a health_status — the field you can check before
sending to decide what to do with the next outbound on that conversation.
While there isn’t a direct link between deliverability and chat health, it is a prediction and analysis of messaging behavior.
You’ll see it on every chat-related webhook event and on every chat read. Treat it as a pre-send gate, not as much a report.
Engagement is a strong signal
Section titled “Engagement is a strong signal”If you take one thing from this page: two-way engagement is among the
strongest signals we use, and it rolls directly
into your line’s reputation. Continuing to send messages into silence is
one of the most common reasons a conversation — and its line — slides to AT_RISK or even
CRITICAL.
So:
- Send messages built to get a reply. Lead with a question or a clear, relevant prompt.
- Let replies set your pace, and back off when they stop. See How many messages should I send? for the cadence and back-off ladder.
How to use it
Section titled “How to use it”We recommend you do whatever is best for your messaging use case, but one example could be to cache the most recent health_status.status from your webhook stream and
check it as a pre-flight before queueing each outbound:
switch (chat.health_status.status) { case 'HEALTHY': send(message); break; case 'AT_RISK': checkReplyRate(); break; case 'CRITICAL': pause(chat); break; case 'OPTED_OUT': skip(chat); // terminal — never resume sending}Acting on the status before each send is what turns the signal into delivery improvement — ultimately leading to a healthier line.
Statuses
Section titled “Statuses”status | What it means | What to do |
|---|---|---|
HEALTHY | Healthy conversation. | Send normally. |
AT_RISK | Poor engagement signals, which may lead to worsening health if current messaging patterns continue. | Slow outbound on this chat and check your reply rate. |
CRITICAL | Strong signals that messages aren’t landing well. | Pause messaging on this chat until healthy. |
OPTED_OUT | The recipient asked you to stop. | Terminal. Stop messaging this recipient — Linq rejects further sends with 2024. |
HEALTHY
Section titled “HEALTHY”The chat looks like a normal conversation. Replies are landing, delivery signals look good, and no opt-out language has been detected. No action needed.
AT_RISK
Section titled “AT_RISK”One or more soft signals suggest this chat is heading in the wrong direction. Common drivers:
- Low engagement. The ratio of recipient replies to your sends is low.
AT_RISK is a warning, not a hard stop. What to do:
- Slow outbound and vary your content. Reduce send frequency; repeated near-identical messages amplify negative signal.
- Back off if replies have stopped. Don’t hold the same cadence into silence — see How many messages should I send?.
Watch for the chat moving back to HEALTHY (good) or down to CRITICAL
(act fast).
CRITICAL
Section titled “CRITICAL”Strong signals that messages on this chat aren’t reaching the recipient the way you expect. Continuing to send is unlikely to help and may make the situation worse for the broader line.
Recommended action: Pause this chat. Re-engage only after chat becomes healthy again.
OPTED_OUT
Section titled “OPTED_OUT”The recipient sent an opt-out keyword on this chat. Regardless of any other signals, do not send further outbound messages to them — the status clears the moment they reply again (see below).
Linq enforces this for you. While a recipient is opted out, every send to
them is rejected with 403 and error code
2024 before the message is queued — nothing is
dispatched. The block covers that recipient in direct messages across every
line on your account, so sending from a different number does not get around
it. Group threads are not affected.
This includes a final courtesy message. There is no free follow-up: if you want
to confirm you have stopped and let the recipient know they can reply any time
to resume, send that one message with override_optout: true. That is the only
way through. It applies to the single request it is set on, does not lift the
block, and each use is recorded against your account.
// the one message you can still sendPOST /v3/chats/{chatId}/messages{ "override_optout": true, "message": { "parts": [{ "type": "text", "value": "You've been unsubscribed. Reply any time if you'd like to hear from us again." }] }}The full set of opt-out keywords (case sensitive, except “optout”):
STOP, UNSUBSCRIBE, OPTOUT, CANCEL, END, QUIT
The keyword must be the entire message, never part of a longer one — STOP
counts, please stop does not. Most keywords must match exactly, including
case. OPT OUT is the exception: it matches in any casing, with or without the
space or a hyphen, so opt out, Opt-Out and optout all count.
The opt-out clears as soon as the recipient replies again. Any later message
from them — in any chat they have with you, the same way the block covers
every chat — opts them back in immediately, unless that message is itself an
opt-out keyword (a restated STOP keeps them opted out). There is no opt-in
keyword to teach: START, OPTIN, and UNSTOP still work in any casing, but
so does any other reply. Once they reply, sends are accepted again and the
chat moves back to whichever health bucket current signals indicate.
Chat health and phone reputation
Section titled “Chat health and phone reputation”Chat health is a leading signal for your line’s reputation:
the health of the conversations on a line rolls up into the line’s overall
reputation. Many AT_RISK or CRITICAL chats on a single line
increase the chance that the line will be flagged by our systems or
carriers.
That said, chat health is not the only thing that affects line reputation. We are continuously improving our models to create a healthy ecosystem. See the Phone Reputation guide for the line-level view.
- New chats start as
HEALTHYand move toAT_RISK,CRITICAL, orOPTED_OUTas signals warrant. updated_attells you when the status last changed; use it to detect rapid status drops in your dashboards.- Switch on
health_statusaccording to the use cases above.
How many messages should I send to keep a chat healthy?
Let replies set the pace. A back-and-forth conversation can sustain a normal cadence; a one-sided one can’t. As a rule of thumb, keep at least 2–3 recipient replies flowing for the volume you send, and don’t send many messages a week into a chat that isn’t replying.
When a recipient goes quiet, slow down and eventually stop — sending harder into
silence is one of the fastest ways to push a chat to AT_RISK or CRITICAL.
Use an escalating back-off:
- No reply? Wait about a day, then send one follow-up.
- Still no reply? Wait a few days, then send one more.
- Still nothing? Send a final message that gives the recipient an easy way out — for example, asking whether they’d like to stop receiving messages — then halt all outbound to that recipient.
- Wait for a reply before sending again. When they respond, read it carefully: a clear “stop” (or an opt-out keyword) means you’re done; genuine interest means you can resume at a normal, reply-paced cadence.
Does chat health read message content or store any PII?
No. Evaluating health status runs on anonymous, aggregate signals — message volume, sends
vs. receives, response cadence, and similar metadata. Inbound text is scanned
at runtime (JIT) to detect opt-out language for OPTED_OUT signals, but
message content is never collected, stored, or retained. No PII is persisted.