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.
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.
Everything demanding attention for a project and the organization behind it, worst first.
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 | 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.
The report
curl "https://api.instasent.com/v1/project/$INSTASENT_PROJECT/attention" \
-H "Authorization: Bearer $INSTASENT_TOKEN"{
"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.
idstringContent 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.
keystringStable 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.
scopestringorg, project or channel. An org item affects every project of the organization, so the same expired card shows up whichever project you asked about.
channelstring | nullsms or rcs, set only on a channel-scoped item; null otherwise.
typestringproblem or activity — what the item is.
severitystringerror, warning or info — how to react: act, act soon, just know.
tagsstring[]A closed vocabulary, constant per key, of which exactly one is a domain tag (billing, compliance, delivery, data, audience, setup).
dismissobjectThe rule's hiding policy — { mode }, plus days when the mode is snooze. See below.
entitiesobject[]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.
dataobjectRule-specific payload, discriminated by key. Absent when the rule carries none.
statestringrunning or finished, on activity items only. Authoritative over endsAt.
progressobject{ done, total }, on activity items only.
startsAtstringWhen the underlying fact began. Absent when unknown.
endsAtstringWhen it ended, or will end, if it is bounded.
dueAtstringThe 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.
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.
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. |
Filtering
Six optional query parameters. All the comma-separated ones are OR within a parameter and AND across parameters.
severitystringComma-separated. Keeps only items of these severities. error,warning
scopestringComma-separated: org, project, channel.
channelstringsms or rcs. Channel is deliberately not a tag value, because it varies per occurrence — it has its own parameter instead.
tagsstringComma-separated. Keeps items carrying all of them.
notTagsstringComma-separated. Drops items carrying any of them. Applied after tags.
countrystringISO 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.
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.