Recommendations
The third question an account asks, after what is left to configure and what has broken: what is worth doing next. Suggestions for a project that already works, best first, from the same engine as attention and served as its own report.
Readiness answers "what is left to configure". Attention answers "what has broken". Neither answers the question a customer starts asking the day after signup and never stops: what is worth doing next.
A project that finished its checklist and has nothing broken used to get silence from both reports — precisely when there was most to say to it. That is what this endpoint carries.
What is worth doing next in a project that already works, best first.
curl "https://api.instasent.com/v1/project/$INSTASENT_PROJECT/recommendations" \
-H "Authorization: Bearer $INSTASENT_TOKEN"{
"entity": {
"items": [
{
"id": "8f2c1a4e9b7d3506",
"key": "first-segment",
"scope": "project",
"channel": null,
"type": "recommendation",
"severity": "info",
"priority": "high",
"summary": "Contacts are loaded but no segment exists yet",
"tags": ["audience"],
"dismiss": { "mode": "dismiss" },
"entities": [],
"data": { "contacts": 1250 }
},
{
"id": "3a91c07f5be2d148",
"key": "sales-tracking",
"scope": "project",
"channel": null,
"type": "recommendation",
"severity": "info",
"priority": "high",
"summary": "No sales are recorded; campaign revenue is unknown",
"tags": ["data"],
"dismiss": { "mode": "dismiss" },
"entities": [],
"data": { "observedWindowDays": 90 }
}
],
"counts": {
"high": 2,
"medium": 0,
"low": 0,
"byKey": { "first-segment": 1, "sales-tracking": 1 }
},
"computedAt": "2026-09-06T09:41:12Z",
"truncated": false
}
}Same engine, separate report
This is not a second engine. It is the attention engine's catalog partitioned by item type: same rule mechanism, same item shape, same fingerprinting, same dismissal contract. Building it twice would have been paid for three times — in code, in documentation and in a second contract to keep in step with the first.
What is deliberately not shared is the response. counts is computed over the whole list in both reports, so a single endpoint returning both kinds and letting you filter would have counted suggestions as problems for everyone who read counts before reading items. Two reports, two sets of counts, no way to conflate them.
The same reasoning applies on the surface: recommendations never appear in a "needs attention" list. A suggestion rendered next to a failure reads as a failure.
key is open here
On an attention item, key comes from a closed enum. Here it is an open string, and that is a contract commitment rather than an oversight. This catalog is meant to grow: a new suggestion must be able to ship without a spec edit landing first in every consumer.
So write the same defensive rendering you would write for a webhook you do not control:
- Never
switchwithout a default. An unrecognised key is a normal event, not an error. - Never drop an unknown item. It is a real suggestion for a real project; a silently discarded one is advice the customer never gets.
- Treat
dataas untyped until you have matched the key. It is discriminated bykey, exactly as in attention.
The keys that exist today travel in attention-catalog.json, published beside the TypeScript definitions — read them from there rather than hard-coding a list that will be stale.
The gate: nothing until the project has started
No recommendation is produced until the project holds contacts and has at least one sender. Before that the response is an empty list with zeroed counts, and that is the correct answer rather than a missing feature.
The reason is that the checklist already owns that phase and does it better. Without the gate, a project created five minutes ago would be handed a list of things it is not doing yet — which is the readiness report's job.
Absence is the point here
The rule that governs attention is that absence is always readiness — for an item to exist, the thing has to exist and be broken.
Recommendations invert it on purpose. "You have contacts and no segment yet" is an absence, and it is exactly the thing worth saying. These are the only rules in the engine allowed to fire on something not existing; the gate is what keeps that from turning into noise.
An item
The shape is the attention item with one field added and several narrowed. Only the differences are described here.
typestringAlways recommendation. It never appears on the attention endpoint, and problem and activity never appear on this one — the catalog is partitioned by this field.
severitystringAlways info. It is kept so the item shape stays identical to an attention item, not because it carries information. Rank by priority.
prioritystringhigh, medium or low, worst first. Declared per key and raised per occurrence when the situation warrants it. This is the only axis the list is ordered by.
summarystringOne English sentence, at most 64 characters, saying what the key is. Declared once per key and identical on every item of it. See summary on the attention page — the same field, the same warning: it is not display copy.
dismissobjectAlways { "mode": "dismiss" }. Never snooze, and never none. See Dismissal.
dueAtstringThe date the suggestion stops being useful, when it has one. Today only the seasonal rule carries it.
id, key, scope, channel, tags, entities, data, startsAt and endsAt behave exactly as they do on an attention item. There is no state and no progress: those belong to activity items.
priority, not severity
An attention item ranks by severity because the question is how hard to react. Every recommendation is info — nothing here is wrong — so ranking by severity would put six identical items in an arbitrary order.
priority answers the other question: of the things worth doing, which first. Items arrive sorted by it and then by the order the catalog declares, so a surface that renders the first three renders the three that matter and does not need to sort.
Dismissal: never on a timer
Every recommendation is a plain dismiss, and the absence of snooze from this catalog is a decision rather than a gap: a suggestion the customer declined must not come back in thirty days as if nobody had asked. An attention item may legitimately return on a timer — an expired card gets worse while looking identical — but nothing here does.
As in attention, the backend stores nothing. If your surface offers hiding, you implement it and you store it, keyed on id.
What brings a dismissed recommendation back is therefore its fingerprint, and each rule chooses that deliberately:
- A rolling measurement carries no date, so it stays hidden until the underlying situation actually changes.
- The seasonal rule carries the year, so next year's Black Friday is a different
idand legitimately reappears. That is the one intended exception to "declined once, gone".
The catalog today
Six keys, and the list is expected to grow — see key is open here.
key | priority | scope | Domain tag | data | Fires when |
|---|---|---|---|---|---|
first-segment | high | project | audience | contacts | There are contacts and no segment, so every campaign goes to everyone. |
sales-tracking | high | project | data | observedWindowDays | No sale has been recorded in the observed window, so campaign return cannot be computed. Requires an authored campaign. |
automate-data-entry | medium | project | data | businessType | Contacts arrive by hand and no platform connector is attached. |
plan-upgrade | medium | org | billing | payingMonths | The organization has been paying month after month on the free plan. |
upgrade-to-rcs | low | project | delivery | — | The project has SMS senders and no RCS agent, has already authored a campaign, and RCS is available to the organization. |
seasonal-campaign | low | project | delivery | occasion, eventName, windowDays | A calendar date worth a campaign is approaching. Black Friday today, from 45 days out. |
Two of them are worth a note each:
-
sales-trackingdoes not mean the customer sells nothing. Every project has an effective definition of a sale, so there is no "tracking is off" state to report — the item says only that no sale was recorded in the lastobservedWindowDays. The common cause is that the sales are arriving, under a different event than the one counted: an order payment, a payment, a lead, a won deal. The action it points at is reviewing which event counts as a sale, not setting anything up.It is also silent rather than pessimistic: when the revenue signals cannot be read at all, the rule emits nothing. "We could not ask" is never published as "they sell nothing", so the absence of this item is not evidence of sales.
-
plan-upgradeis the only one that reads the organization, so it isscope: "org"and carries anorganizationentity. It is also the only one that skips the gate and the only one that applies toapi_smsprojects, which have no audience and no authored campaigns; on such a project it is the only recommendation you will ever see.
No filters, and do not poll it
The endpoint takes no query parameters at all — no severity, no scope, no tags, none of the six that attention accepts. The list is short by construction and ordered for you; filtering it server-side would only give counts a way to disagree with items.
There is no debug parameter on the Product API. The dashboard has a sample view so a developer can render every card, and it stays there: an external agent handed synthetic items would repeat them to a customer as fact.
Rate limit is 60 requests a minute, and it is generous rather than tight — this report moves over days, not minutes. Fetch it when a surface opens, not on an interval. computedAt tells you how old the answer you are holding is; truncated tells you the list was capped, which is not something today's six keys can reach.