# 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, expand-sender-coverage, sender coverage, register a sender in more countries, seasonal-campaign, black friday, cyber monday, christmas, halloween, valentine's day, summer sales, winter sales, occasion, summary, dismiss a recommendation, why do I see no recommendations, gate, audience and channel, attention vs recommendations
**Related pages:** /platform-api/product-api/project-setup/attention, /platform-api/product-api/project-setup/readiness, /platform-api/product-api/project-setup/channel-readiness
**Docs index (every page):** https://staging-instasent-docs-nextjs.oscar-284.workers.dev/llms.txt
**This zone's index:** https://staging-instasent-docs-nextjs.oscar-284.workers.dev/platform-api/product-api/llms-full.txt
**This page:** https://staging-instasent-docs-nextjs.oscar-284.workers.dev/platform-api/product-api/project-setup/recommendations/ (HTML) · https://staging-instasent-docs-nextjs.oscar-284.workers.dev/platform-api/product-api/project-setup/recommendations.md (Markdown)

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

Eight 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`                                       | 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`](/platform-api/product-api/flows/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-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.
- **`expand-sender-coverage` comes once per sender, per channel.** `channel` is `sms` or `rcs`, and the sender is the `sender` entity. The countries are the ones the [channel readiness report](/platform-api/product-api/project-setup/channel-readiness#warnings) recommends registering for that sender, kept only where the audience is real (at least 1% and 25 contacts) and listed largest audience first. `audienceShare` is 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-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` (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-campaign` comes once per occasion.** `occasion` is a stable id: `black_friday`, `christmas` and `summer_sales` appear 45 days before the date; `cyber_monday`, `winter_sales`, `valentines_day` and `halloween` appear 30 days before. `windowDays` is that lead time. The list can grow, so handle an `occasion` you do not recognise gracefully, for example by falling back to `eventName`. `eventName` is the occasion's name in English and `eventNameEs` in Spanish; when there is no Spanish name, `eventNameEs` carries 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 as `2026-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 at `low` and rises to `medium` within 14 days of its date.

> **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](/platform-api/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 today's catalog does not reach in practice.

---

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