# Attention

One GET returns everything currently wrong with a project and the organization behind it — an expired card, a connector that stopped syncing, a scheduled campaign the balance will not cover — worst first, with an absolute deadline wherever one exists.

**Language:** en
**Audience:** developer
**TLDR:** GET /v1/project/{project}/attention returns items[], counts and computedAt. One item is one occurrence, not one rule with rows: two connectors needing a reconnect are two items sharing a key. key discriminates data; type (problem | activity) and severity (error | warning | info) are independent axes; tags are closed and constant per key. Nothing is persisted and there is no inbox — an item disappears when the fact resolves. Dismissal is the client's business: the backend declares a policy in dismiss and stores none of it. Filter with severity, scope, channel, tags, notTags and country; an unknown value is a 400, and counts is always computed over the whole list.
**Search keywords:** attention, what needs attention, alerts, what is broken, why did my campaign not go out, expired card, low balance, unpaid invoice, overdue invoice, connector disconnected, stale datasource, reconnect, pending connection, scheduled campaign underfunded, insufficient funds, auto charge disabled, api blocked, rejected registration, pending review, regulation deadline, dismiss, snooze, counts, occurrence, deadline, dueAt, health check, is everything ok
**Related pages:** /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/attention/ (HTML) · https://docs.instasent.com/developers/product-api/project-setup/attention.md (Markdown)

The readiness endpoints answer "what is left to configure". This one answers the other half: **what has broken, or is about to, and by when.** An expired card, a connector that has stopped bringing contacts in, a campaign scheduled for tomorrow that the balance will not cover.

It is the endpoint behind a health check — the one an integration calls to answer "is everything OK with this account", and the one an assistant reads before claiming that it is.

[`GET /v1/project/{project}/attention` - Everything demanding attention for a project and the organization behind it, worst first.](/developers/product-api/reference)

Any Product API token with access to the project can read it; no extra scope is required. Note the range: the report covers the project **and the organization behind it**, so a token scoped to one project still sees the organization's expired card. It has to — the card is what stops that project from sending.

## The boundary with readiness

The two engines split the work and never report the same fact twice.

|                                                              | Question                         | Example                               |
| ------------------------------------------------------------ | -------------------------------- | ------------------------------------- |
| [Readiness](/developers/product-api/project-setup/readiness) | What is left to configure?       | "You have no legal profile yet"       |
| **Attention**                                                | What has broken, or is about to? | "The card on file expired on 1 April" |

The rule that separates them is worth holding on to, because it is the one that decides where a new fact lands: **absence is always readiness.** For an attention item to exist, the thing has to exist *and* be broken. There is no expired card without a card, no disconnected connector without a connector, no underfunded campaign without a campaign.

The practical consequence is the one to design your UI around: **a project that has just finished signup has a full checklist and no alerts at all.** You will never render "your balance is low" next to the step asking the customer to add funds. If you ever see an error on a brand-new project, that is a bug on our side, not an edge case to handle.

> **Note**: There is one deliberate overlap, and it is a handover rather than a duplicate. A rejected registration is reported here as news, with its cause, for 30 days; at the same time readiness lists that country under `recommended-registrations`, because a rejected registration is not coverage. Both statements are true and they say different things. See [what moved here](/developers/product-api/project-setup/channel-readiness#what-moved-to-the-attention-endpoint).

## The report

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

```json
{
  "entity": {
    "items": [
      {
        "id": "3f2a1b0c9d8e7f65",
        "key": "expired-card",
        "scope": "org",
        "channel": null,
        "type": "problem",
        "severity": "error",
        "tags": ["billing", "entity:organization", "expired-payment-method"],
        "dismiss": { "mode": "snooze", "days": 14 },
        "entities": [
          { "type": "card", "id": "pm_1a2b3c", "name": "Visa" },
          { "type": "organization", "id": "5f3a9b2c4d1e0f00112233ab", "name": "ACME" }
        ],
        "data": { "brand": "Visa", "last4": "4242", "expiresOn": "2026-03", "default": true },
        "startsAt": "2026-04-01T00:00:00Z"
      },
      {
        "id": "91bc44de20fa5c37",
        "key": "scheduled-campaign-underfunded",
        "scope": "project",
        "channel": null,
        "type": "problem",
        "severity": "error",
        "tags": ["billing", "entity:campaign", "insufficient-funds"],
        "dismiss": { "mode": "none" },
        "entities": [
          { "type": "campaign", "id": "6512ab34cd56ef7890123456", "name": "Black Friday" }
        ],
        "data": { "estimatedCost": 210.0, "shortfall": 60.0, "currency": "EUR" },
        "dueAt": "2026-09-06T09:00:00Z"
      }
    ],
    "counts": {
      "error": 2,
      "warning": 1,
      "info": 0,
      "byKey": { "expired-card": 1, "scheduled-campaign-underfunded": 1, "pending-connection": 1 }
    },
    "computedAt": "2026-09-05T09:41:12Z",
    "truncated": false
  }
}
```

`items` arrives sorted worst first, then soonest deadline, then key. Render it in the order it arrives.

## Nothing is stored

There is no inbox behind this. No table of alerts anyone marks as read, no unread count to reconcile: **every call recomputes the whole thing from the account's current data.**

That has one consequence that should shape how you build on it: **an item disappears when the fact resolves, and only then.** The expired-card item goes when the customer adds another card. The half-finished connection goes when the data source is created. Nobody has to remember to turn anything off, and nothing can be left behind pointing at a problem that no longer exists.

It also means the inverse is guaranteed, which is the part that matters if an assistant is reading this: **if an item is in the response, it is true right now.** A stored inbox drifts from reality and eventually lies — it holds "card expired" from three weeks ago, the customer replaced the card, and someone opens a conversation about a problem that does not exist. That cannot happen here.

`computedAt` travels for exactly that reason. It is always "now" at assembly time, but a client that fetches once and renders for the next ten minutes is holding an answer that is ten minutes old and needs to know it.

## An item is one occurrence

**One item is one occurrence of one rule, not one rule with a list of rows.** Two connectors that both need reconnecting are two items sharing a `key`. If you want them rendered as one block, group by `key` yourself.

- `id` — `string`
  Content fingerprint of this occurrence — stable across polls while it is the same problem, and defined per key. An expired card is identified by brand, last4 and expiry rather than by the card's id, so re-adding the same card stays dismissed while a *different* expired card resurfaces. Nothing volatile goes into it, so it is safe to use as a React key or as the handle for a stored dismissal.
- `key` — `string`
  Stable rule identifier, and the discriminator for `data`. **The API ships no copy**: the label and its translation are yours.
  
  Render an unknown key rather than dropping it — the backend can ship a new one ahead of its consumers, and a silently discarded item is a problem the customer never hears about.
- `scope` — `string`
  `org`, `project` or `channel`. An `org` item affects every project of the organization, so the same expired card shows up whichever project you asked about.
- `channel` — `string | null`
  `sms` or `rcs`, set only on a channel-scoped item; `null` otherwise.
- `type` — `string`
  `problem` or `activity` — what the item *is*.
- `severity` — `string`
  `error`, `warning` or `info` — how to react: act, act soon, just know.
- `tags` — `string[]`
  A closed vocabulary, **constant per key**, of which exactly one is a domain tag (`billing`, `compliance`, `delivery`, `data`, `audience`, `setup`).
- `dismiss` — `object`
  The rule's hiding policy — `{ mode }`, plus `days` when the mode is `snooze`. See [below](#the-dismiss-field).
- `entities` — `object[]`
  What the item is about, so you can link straight to it without parsing copy. Each carries `type`, `id` and, when there is one, `name`. `name` is the entity's own display name — customer-written text, never personal data.
- `data` — `object`
  Rule-specific payload, **discriminated by `key`**. Absent when the rule carries none.
- `state` — `string`
  `running` or `finished`, on `activity` items only. Authoritative over `endsAt`.
- `progress` — `object`
  `{ done, total }`, on `activity` items only.
- `startsAt` — `string`
  When the underlying fact began. Absent when unknown.
- `endsAt` — `string`
  When it ended, or will end, if it is bounded.
- `dueAt` — `string`
  The deadline after which the consequence lands.

### `type` and `severity` are independent axes

They answer different questions and you need both. An import that is running is `activity` / `info`; the same import stalled is `activity` / `warning`. Do not collapse one into the other, and do not derive either from `tags`: the tags are constant per key precisely so they can be used for filtering and grouping, never as a severity signal in disguise.

### Switch on `key`, never on which fields are missing

`data` is discriminated by `key`. Narrow on the key first and the payload shape narrows with it. Inferring the payload from which fields came back absent breaks the moment a rule gains a field.

### Timestamps are absolute, and absent rather than null

Every timestamp is absolute UTC, and **omitted rather than sent as `null`** when it is unknown. Nothing relative is ever sent — no "12 days left" — because the payload outlives the instant it was computed: a countdown stops being true as soon as anything stores it or repeats it a few minutes later. Compute the relative form at render time, against the report's `computedAt`.

> **Tip**: `dueAt` is where most of the value is. An underfunded campaign carries its send time, an expiring card carries the expiry, a regulator deadline carries the date the country closes. Sorting or badging on `dueAt` is what turns this endpoint from a status list into a warning that arrives while the customer can still act.

## The `dismiss` field

**There is no dismiss endpoint, and the backend stores nothing.** If your surface offers a way to hide an item, you implement it and you store it — the API never learns it happened.

What the field gives you is the rule's policy, so that every surface makes the same call rather than each inventing one. It is constant per `key`.

| `mode`    | What you may offer                                                                                                                                         |
| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `none`    | Nothing. Hiding it would only delay the consequence — an overdue invoice, a blocked API.                                                                   |
| `dismiss` | Hide with no expiry. Bring it back when the content changes, which is what `id` tracks.                                                                    |
| `snooze`  | Hide for `days`, then bring it back even if nothing changed. For facts that worsen on their own while their fingerprint stays identical — an expired card. |

A snooze is a **floor, not a ceiling**: if the content changes inside the window the `id` changes, and an item you keyed on `id` reappears early on its own. Since the expiry is computed by whoever stores the dismissal, changing `days` here is not retroactive over what you already stored.

> **Warning**: **This response is never filtered by anyone's dismissals.** It is the full list, always. That is what makes it safe to build a health check, a support tool or an assistant on top of it: none of them can be blinded by a person who chose not to look at something. It also means that if you render it raw, you render everything — hiding is yours to add.

## The catalogue

Sixteen keys, grouped by the domain tag they carry. The condition is what makes each one appear.

### `billing`

| `key`                            | Appears when                                                                                                                                                                                                                                      |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `expired-card`                   | A stored card's expiry date has passed.                                                                                                                                                                                                           |
| `expiring-card`                  | The same card, 30 days before.                                                                                                                                                                                                                    |
| `low-balance`                    | Below the organization's configured threshold, and **only if money ever entered the account**. Zero on a brand-new account is not low balance, it is an unfunded account — that is readiness. On postpaid it looks at consumed allowance instead. |
| `subscription-past-due`          | A paid subscription's charge failed and the subscription is not frozen.                                                                                                                                                                           |
| `unpaid-invoice`                 | A charge is outstanding with an invoice, due date still ahead.                                                                                                                                                                                    |
| `overdue-invoice`                | The same, past its due date. An invoice with no due date is **never** marked overdue.                                                                                                                                                             |
| `auto-charge-disabled`           | Prepaid only, only when the project has active automations or flows, and only when the organization is actually able to turn it on.                                                                                                               |
| `scheduled-campaign-underfunded` | The cost of the scheduled campaigns exceeds available balance. One item per campaign that is not covered, carrying its send time as `dueAt`.                                                                                                      |
| `campaign-unpaid`                | A campaign went unpaid during its send. Shown for 30 days.                                                                                                                                                                                        |

### `data`

| `key`                  | Appears when                                                                                                                                   |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `pending-connection`   | The customer completed an integration's authorization and never created the data source. The abandoned authorization is the thing that exists. |
| `datasource-reconnect` | The data source exists and its access has dropped or expired, so it is bringing nothing in.                                                    |
| `stale-datasource`     | Seven days without bringing data in, and **only if it ever synced**. One that never synced is unfinished setup, not a fault.                   |

### `delivery`

| `key`                    | Appears when                                                                                                                                        |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `api-rate-limit-blocked` | The account carries a manually set negative limit, which rejects every call. **Never appears on a normal account**: if you see it, somebody set it. |

### `compliance`

| `key`                   | Appears when                                                                                                                                                                                                                              |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `rejected-registration` | A carrier or a regulator turned a registration down. Shown for 30 days, then the fact returns to readiness as a country still to register.                                                                                                |
| `pending-review`        | A registration was submitted and is waiting on a carrier or a regulator. The catalogue's only informative entry — nothing is broken and there is nothing to do.                                                                           |
| `regulation-deadline`   | Only in the 30 days before the date, and only for countries the customer **can send to today**. It says "on the 15th you stop being able to send to Spain", which is not the same as "you have not registered Spain" — that is readiness. |

> **Note**: `rejected-registration` and `pending-review` kept the key names they had as channel-readiness warnings before 2026-09-05, so a switch written against the old endpoint still matches. What changed is the shape around them: an occurrence with `entities` and `data`, not a warning with `rows`.

## Filtering

Six optional query parameters. All the comma-separated ones are OR within a parameter and AND across parameters.

- `severity` — `string`
  Comma-separated. Keeps only items of these severities. `error,warning`
- `scope` — `string`
  Comma-separated: `org`, `project`, `channel`.
- `channel` — `string`
  `sms` or `rcs`. Channel is deliberately not a tag value, because it varies per occurrence — it has its own parameter instead.
- `tags` — `string`
  Comma-separated. Keeps items carrying **all** of them.
- `notTags` — `string`
  Comma-separated. Drops items carrying **any** of them. Applied after `tags`.
- `country` — `string`
  ISO 3166-1 alpha-2. Country, like channel, is not a tag value.

Two things are worth knowing before you build on this.

**Filtering is free, and it does not save you a call.** The list is assembled first and filtered afterwards, so a filtered request costs exactly what an unfiltered one costs. Filter for the sake of your own rendering, not to make the endpoint cheaper.

**`counts` is always computed over the whole list**, never over the filtered one. A surface that asked for one tag still learns there are errors elsewhere, without paying for a second call to find out.

> **Warning**: An unknown value in any filter is a `400`, not a silent empty list. That is on purpose: a typo in a tag name should fail loudly rather than look exactly like a healthy account.

## `truncated`

`truncated: true` means the engine hit its per-report ceiling and the list you got is not everything. Treat it as "at least this much is wrong", and never render a total from `items.length` when it is set — `counts` is the number to trust.

---

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.
