Platform API · Product API · Project setup
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, live and procedure 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. No item on this endpoint carries state or progress: those fields are optional on any item, and no recommendation emits them.
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
Eight 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 | Every data source the customer created is a CSV upload, and there is at least one. Anything else they set up silences it — a native connector, a webhook, or an Ingest API source (the panel calls that one "Instasent API"). The three invisible system rows (api, instasent, defaults) never count, in either direction. |
plan-upgrade | medium | org | billing | payingMonths | The organization has been paying month after month on the free plan. |
expand-sender-coverage | medium | channel | delivery | countries, audienceShare | A sender is not registered in countries that together hold more than 10% of the contacts with a mobile phone. One item per sender, per channel. |
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. |
flow-template | medium | project | delivery | templateKey, title, eventType, datasourceId | A ready-made flow fits an event the project already receives, and no flow was created from it. One item per template, at most three: exactly the ones GET /flow/templates flags recommended. Only where flows are available to the project. |
seasonal-campaign | low | project | delivery | occasion, eventName, eventNameEs, windowDays | A calendar date worth a campaign is approaching. Seven occasions today, each from 30 or 45 days out. One item per occasion. |
Four 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.
-
expand-sender-coveragecomes once per sender, per channel.channelissmsorrcs, and the sender is thesenderentity. The countries are the ones the channel readiness report recommends registering for that sender, kept only where the audience is real (at least 1% and 25 contacts) and listed largest audience first.audienceShareis their combined share of the contacts with a mobile phone, from 0 to 1 and unrounded. A country recommended because a regulator requires it counts with its audience like any other. Dismissing it holds until that sender's list of countries changes; a change in the percentages alone does not bring it back.Its absence is not proof of full coverage: when the audience figures are not available at that moment, the item is left out of that response rather than guessed.
-
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_sms(A2P Messaging) projects, which have no audience and no authored campaigns; on such a project it is the only recommendation you will ever see. -
seasonal-campaigncomes once per occasion.occasionis a stable id:black_friday,christmasandsummer_salesappear 45 days before the date;cyber_monday,winter_sales,valentines_dayandhalloweenappear 30 days before.windowDaysis that lead time. The list can grow, so handle anoccasionyou do not recognise gracefully, for example by falling back toeventName.eventNameis the occasion's name in English andeventNameEsin Spanish; when there is no Spanish name,eventNameEscarries the English one, so it is always a string.The date of the occasion is the item's
dueAt: midnight of that day in the project's timezone, expressed in UTC. For a project in Madrid, Cyber Monday 2026 arrives as2026-11-29T23:00:00Z, so convert it to the project's timezone before taking the calendar day. Several occasions can be active at once (in November, Black Friday, Cyber Monday and Christmas are three separate items). Each one starts atlowand rises tomediumwithin 14 days of its date.
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 today's catalog does not reach in practice.