# 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.

**Language:** en
**Audience:** developer
**TLDR:** GET /v1/project/{project}/recommendations returns items[], counts and computedAt. Same engine and same item shape as attention, a separate endpoint so neither report's counts can contain the other's items. Three differences that matter: key is an OPEN string here because the catalog is meant to grow without a spec edit; absence is the point rather than forbidden, held back by a gate — nothing is suggested until the project has contacts and a sender; and every item is severity info, ranked by priority (high | medium | low) instead. dismiss is always { mode: dismiss }: a suggestion must never come back on a timer. No filters, no debug parameter, 60 requests a minute, and it changes over days rather than minutes — do not poll it.
**Search keywords:** recommendations, what should I do next, next steps, suggestions, grow, priority, high medium low, open key, catalog, first-segment, sales-tracking, automate-data-entry, plan-upgrade, upgrade-to-rcs, seasonal-campaign, black friday, summary, dismiss a recommendation, why do I see no recommendations, gate, audience and channel, attention vs recommendations
**Related pages:** /developers/product-api/project-setup/attention, /developers/product-api/project-setup/readiness, /developers/product-api/project-setup/channel-readiness
**Docs index (every page):** https://docs.instasent.com/llms.txt
**This zone's index:** https://docs.instasent.com/developers/product-api/llms-full.txt
**This page:** https://docs.instasent.com/developers/product-api/project-setup/recommendations/ (HTML) · https://docs.instasent.com/developers/product-api/project-setup/recommendations.md (Markdown)

[Readiness](/developers/product-api/project-setup/readiness) answers "what is left to configure". [Attention](/developers/product-api/project-setup/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.

> **Warning**: **Three different things in this API share the word.** They are not related and no page mixes them:
> 
> - **`recommendation`** — the item `type` on this page: a next step worth taking. This endpoint.
> - **`recommended-registrations`** — a [readiness warning key](/developers/product-api/project-setup/channel-readiness#warnings): countries a sender ought to be registered for and is not. A coverage gap, not a suggestion, and it lives in the channel report.
> - **`recommendations`** — a field in the MCP server's channel-readiness tool, which lists exactly those registrations. Same subject as the warning above, nothing to do with this endpoint.
> 
> If you are looking for countries to register, you want the [channel report](/developers/product-api/project-setup/channel-readiness), not this one.

[`GET /v1/project/{project}/recommendations` - What is worth doing next in a project that already works, best first.](/developers/product-api/reference)

```bash
curl "https://api.instasent.com/v1/project/$INSTASENT_PROJECT/recommendations" \
  -H "Authorization: Bearer $INSTASENT_TOKEN"
```

```json
{
  "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.

> **Note**: If you already consume [attention](/developers/product-api/project-setup/attention), almost everything you wrote is reusable here. The three differences worth reading before you do are [the open `key`](#key-is-open-here), [the gate](#the-gate-nothing-until-the-project-has-started) and [`priority` instead of `severity`](#priority-not-severity).

## `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 `switch` without 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 `data` as untyped until you have matched the key.** It is discriminated by `key`, 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.

> **Warning**: **The gate is asked of reality, never of the checklist.** It reads actual contacts and actual senders, not whether the corresponding readiness steps are marked complete. Those are different questions: readiness steps can be skipped, a skip is shared across the organization, and the step catalog itself differs by project type. A gate that read the checklist would answer differently for two projects in identical states.

### Absence is the point here

The rule that governs [attention](/developers/product-api/project-setup/attention#the-boundary-with-readiness) 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](/developers/product-api/project-setup/attention#an-item-is-one-occurrence) with one field added and several narrowed. Only the differences are described here.

- `type` — `string`
  Always `recommendation`. It never appears on the attention endpoint, and `problem` and `activity` never appear on this one — the catalog is partitioned by this field.
- `severity` — `string`
  Always `info`. It is kept so the item shape stays identical to an attention item, not because it carries information. Rank by `priority`.
- `priority` — `string`
  `high`, `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.**
- `summary` — `string`
  One English sentence, at most 64 characters, saying what the key *is*. Declared once per key and identical on every item of it. See [`summary`](/developers/product-api/project-setup/attention#summary-is-not-your-copy) on the attention page — the same field, the same warning: it is not display copy.
- `dismiss` — `object`
  Always `{ "mode": "dismiss" }`. Never `snooze`, and never `none`. See [Dismissal](#dismissal-never-on-a-timer).
- `dueAt` — `string`
  The 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](/developers/product-api/project-setup/attention#the-dismiss-field). 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 `id` and legitimately reappears. That is the one intended exception to "declined once, gone".

> **Warning**: **This response is never filtered by anyone's dismissals**, exactly like attention. If you render it raw you render everything, including what the customer hid in the dashboard — the two surfaces are not synchronised, and neither one is blinded by the other.

## The catalog today

Six keys, and the list is expected to grow — see [`key` is open here](#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-tracking` does 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 last `observedWindowDays`. 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-upgrade` is the only one that reads the organization**, so it is `scope: "org"` and carries an `organization` entity. It is also the only one that skips the gate and the only one that applies to `api_sms` projects, which have no audience and no authored campaigns; on such a project it is the only recommendation you will ever see.

> **Note**: **`upgrade-to-rcs` is `scope: "project"` and still sets `channel: "rcs"`.** The scope says whose situation it is; `channel` says what it is about. Do not infer one from the other.

## 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](/developers/product-api/project-setup/attention#filtering). 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.

---

This is one page of the Instasent documentation. For the complete machine-readable index of every guide and API reference, fetch https://docs.instasent.com/llms.txt — start there for full context.
