Skip to content

Attention API

Applies to: All subscriptions

Purpose

Describe /api/attention — the API for compliance findings: what is open, which documents are affected, who owns each item, and how it closed.

Three facts that decide how you use it:

  1. A finding cannot be closed by an API call saying "fixed". It closes by itself when Complifly re-checks and the problem is gone. The only manual close is a decision (for example "IRN not required"), and only for the kinds of finding that list one.
  2. Findings exist only where the scheduled check is switched on. Eight rules are available today — IRN_WINDOW, IRN_WINDOW_PASSED, DOC_REJECTED_NIC, QUARANTINE_HELD, EWB_EXPIRY, EWB_MISSING_FOR_IRN, EWB_PART_B_PENDING and ERP_WRITEBACK_FAILED (see Rules). Findings are created by the scheduled check, which is off until an operator enables it (COMPLIANCE_SWEEP_ENABLED). Until then GET /summary returns no groups and sweepHealthy: false. A registration sees the screen built on this API only where COMPLIANCE_INTELLIGENCE_V2 is switched on (off by default; see Feature Flags); every other registration still sees the earlier screen, served by the Compliance Alerts API.
  3. An empty list is only trustworthy when sweepHealthy is true. If the scheduled check is switched off or stale, "no findings" means "not checked", not "nothing wrong".

These are application endpoints, not a versioned integration contract (see Endpoint Index).

Audience

Integration developers feeding findings into a ticketing or reporting system, and administrators scripting bulk acknowledgement.

Prerequisites

  • API Authentication — a user token. There is no machine-to-machine access to this API: every lifecycle action is recorded against a named user.
  • An x-gstin header naming one of the caller's GSTINs. The cross-tenant value all is refused with GSTIN_REQUIRED.
  • Permissions (see Roles and Permissions):
To The caller's role needs
Read anything intelligence.view (every role) and the document permission for that kind of finding (irn.view, ewb.view, …) — a finding about documents you cannot open is not returned
Acknowledge, assign, add a note attention.act
Record a decision, reopen attention.dispose and the document right for that finding (irn.generate, ewb.generate, …)
Run a check now admin.view

Reference

Method Path Purpose
GET /api/attention/summary Open findings grouped by rule — one group per alert
GET /api/attention/findings The findings behind a group
GET /api/attention/findings/{id} One finding: evidence, timeline, other open findings on the same document
POST /api/attention/findings/{id}/acknowledge Take ownership
POST /api/attention/findings/{id}/assign Assign to a user
POST /api/attention/findings/{id}/unassign Remove the assignee
POST /api/attention/findings/{id}/notes Add a note to the timeline
POST /api/attention/findings/{id}/dispose Close with a recorded decision
POST /api/attention/findings/{id}/reopen Reopen a decision (within 30 days)
POST /api/attention/findings/bulk The same actions on up to 500 findings
GET /api/attention/history Closed findings; ?format=csv downloads them
GET /api/attention/findings?format=csv The open list as a file, with the same filters
GET /api/attention/rules The kinds of finding the caller can see, and the decisions each allows
GET /api/attention/assignees Users a finding on this GSTIN can be assigned to
GET /api/attention/state When this GSTIN was last checked
POST /api/attention/evaluate Check this GSTIN now (once a minute)
GET /api/attention/overview Every registration the caller may see, one line each, with the totals added up
GET /api/attention/notification-profiles/user The caller's own notification settings, and the lists a screen renders from
GET /api/attention/notification-profiles/gstin/{gstin} Who is emailed about one registration
GET /api/attention/notification-profiles/org/{companyId} Who is emailed for a whole organization
PUT /api/attention/notification-profiles/user/{event}, /gstin/{gstin}/{event}, /org/{companyId}/{event} Create or replace one notification profile
DELETE the same three paths Remove one
POST the same three paths, with /resend appended Send a new confirmation link to an address still waiting
GET /api/attention/notifications/verify?token= No login. The link in a confirmation email
GET /api/attention/notifications/unsubscribe?token= No login. The link at the foot of an email to an address

Rules

A rule decides what is a finding, how severe it is, what closes it, and which decisions it accepts. GET /rules returns the ones the caller can see.

ruleKey A finding is Severity Closes by itself when Decisions accepted (resolutionCode)
IRN_WINDOW A document that needs an IRN, has none, and has 7 days or fewer before the 30-day deadline. Same scope and location values as IRN_WINDOW_PASSED CRITICAL with 3 days or fewer left (the last day included), otherwise HIGH — recomputed every pass, so it escalates on its own An IRN is recorded; the document date is corrected out of the window; a quarantined document is accepted; the ERP re-sends it successfully. Crossing the deadline closes it (EXPIRED, close_reason: DEADLINE_PASSED) and opens an IRN_WINDOW_PASSED finding for the same document None. A dispose call returns 400 INVALID_RESOLUTION_CODE — generate the IRN instead
ERP_WRITEBACK_FAILED An update Complifly tried to send to your ERP (an IRN, an e-way bill, a refusal) and gave up on, because of your side: the ERP refused it, its endpoint was unreachable until the retries ran out, or Complifly would not call the address. One finding per dead event (location: ERP_WRITEBACK). Causes Complifly must investigate are not listed HIGH The event is delivered after a requeue (REPLAY_DELIVERED); it is requeued (REQUEUED); it is skipped by an operator (SKIPPED_BY_OPERATOR); a later event for the same document and the same fact is delivered (SUPERSEDED); the event row is purged (SOURCE_DISCARDED) None. Replay, requeue and skip are done on the Operations screen, where a reason is mandatory
EWB_PART_B_PENDING A live e-way bill generated more than 24 hours ago whose Part-B is missing: no vehicle number for a road movement (or no mode recorded), or no transport document for rail, air or ship. One finding per e-way bill. Bills already past the life of Part-A are not raised HIGH with 24 hours or fewer to the due time, otherwise MEDIUM Part-B is entered (PART_B_UPDATED); the bill is cancelled (CANCELLED) or delivered (DELIVERED); the row is gone (SOURCE_DISCARDED); Part-A runs out (EXPIRED, PART_A_LAPSED) None. A dispose call returns 400 INVALID_RESOLUTION_CODE — enter the vehicle
EWB_MISSING_FOR_IRN A document with no e-way bill, though goods are probably moving. Two kinds, one finding per document (location): an invoice with an IRN, goods on it and no e-way bill, where the document declares transport or its value is ₹50,000 or more, with the IRN acknowledged between 7 days and 4 hours ago (IRN_INVOICE); and a document that exists only to get an e-way bill whose latest attempt failed in a way the customer can fix (EWB_DOCUMENT) HIGH An e-way bill now exists (EWB_GENERATED); the IRN is cancelled (IRN_CANCELLED); the 7-day look-back elapses (EXPIRED, LOOKBACK_ELAPSED); the document is gone (SOURCE_DISCARDED); it is edited so it no longer qualifies (DOCUMENT_CHANGED) EWB_GENERATED_ELSEWHERE (needs evidence.ewbNo, 12 digits), NO_MOVEMENT_REQUIRED, BELOW_STATE_THRESHOLD, SERVICE_SUPPLY (each needs a note)
EWB_EXPIRY A live e-way bill (status ACTIVE, ACT or SUCCESS) whose validity ends within 24 hours, or ended up to 8 hours ago and can still be extended. One finding per e-way bill CRITICAL with 8 hours or fewer left (including up to 8 hours past expiry), otherwise MEDIUM — recomputed every pass, so it escalates on its own The validity is extended past 24 hours (EXTENDED); the bill is delivered (DELIVERED) or cancelled (CANCELLED); the row is gone (SOURCE_DISCARDED). If the extension window closes while it is still active it closes EXPIRED / EXTENSION_WINDOW_LAPSED and is kept 8 years None. A dispose call returns 400 INVALID_RESOLUTION_CODE — extend the bill or mark it delivered
QUARANTINE_HELD A document the customer sent that was refused before it became a document: an Excel row that failed validation (location: QUARANTINE) or an ERP push refused at the API (location: ERP_REJECTED). Anything blocked counts — no document-type or supply-type filter. Raised only after it has been held for 4 hours CRITICAL when the upload carries an IRN deadline (the document needs an IRN and does not already exist) with 3 days or fewer left, otherwise HIGH. Past the deadline it is HIGH. A hold whose only error is that the number already exists is MEDIUM and never CRITICAL The row is promoted to a document (PROMOTED); the ERP re-sends it successfully (SUPERSEDED); a newer upload of the same document replaces it (DOCUMENT_CHANGED); it is discarded, purged or gone (EXPIRED, SOURCE_DISCARDED) None. A dispose call returns 400 INVALID_RESOLUTION_CODE — correct or discard it in the Correction Cockpit
DOC_REJECTED_NIC An invoice, credit note or debit note NIC refused because of its content, still inside the 30-day deadline. One finding per document. Only refusals the customer can fix — failures of NIC or the GSP (service under maintenance, "application error — try again", login errors) are not listed CRITICAL with 3 days or fewer to the IRN deadline, otherwise HIGH An IRN is recorded; the document is resubmitted and NIC's latest answer is no longer a refusal of its content (RESUBMITTED); the document is edited out of scope or deleted; it crosses the deadline (EXPIRED, DEADLINE_PASSED — it then appears as IRN_WINDOW_PASSED) WITHDRAWN (needs a note): the document will not be reported
IRN_WINDOW_PASSED A document that needs an IRN, is more than 30 days past its date, and has none. One finding per document, wherever it is stuck (location) HIGH An IRN is recorded; the document date is corrected so it is no longer overdue; a quarantined document is accepted or discarded; the ERP re-sends it successfully REISSUED (needs evidence.newDocNo), IRN_NOT_REQUIRED, CANCELLED_IN_BOOKS, EXCEPTION_RECORDED (each needs a note)

ERP_WRITEBACK_FAILED in detail

Boundary Customer action is here; platform health is not. Raised: the ERP answered and refused it (ERP_REJECTED, fixed by you), the endpoint was unreachable until retries ran out or Complifly refused the address (fixed by your administrator), and any cause that cannot be classified. Not raised: outcome AMBIGUOUS (the ERP may have received it) and delivery that simply gave up (DEAD) — Complifly investigates those. A whole connection failing is not a finding: it is the operations alert, a count per connection
Who fixes it evidence.fixedBy (you, admin, or null when the Help Centre has no entry for the code) and evidence.help
evidence eventType, subscriptionId, sourceSystem, externalRef, occurredAt, attempts, lastErrorCode, lastError, help, fixedBy
What closes it Read from what the system does: a requeue returns the same event to the queue; a rebuild and replay leaves the dead event DEAD as the audit record and sends a new STATE_RESYNC, so the finding closes because a later delivery of the same fact (or a STATE_RESYNC) exists. A later delivery of a different fact does not close it — a delivered e-way bill does not tell your ERP the IRN
Timing Dead-lettering, requeue and skip are seen at once. A replay that delivers a new event is seen at the next scheduled check
Needs to see it admin.api or irn.generate (the tax team should know the ERP lacks the IRN)
Needs to act Acknowledge, assign and note: attention.act. This rule defines no allowed decisions, so no decision can be recorded here by any role: a request to dispose of one is refused (400 INVALID_RESOLUTION_CODE, "cannot be closed by hand"). Before 2026-10-15 a 403 said the same thing through the roles (no role held both attention.dispose and admin.api); a Super Admin now holds both and gets the 400 instead
Enabled by ERP_WRITEBACK_ENABLED for the registration. It is off by default; while it is off, no finding exists and any existing one expires
Kept 1 year (OPERATIONAL)

EWB_PART_B_PENDING in detail

Part-B missing Road (trans_mode 1, or none): no vehicle number. Rail, air, ship (2, 3, 4): no transport document number. A blank value counts as empty. The other mode's field is ignored: a vehicle on a rail bill does not count
due_at Generation time + partAValidityDays (default 15), as a UTC instant. The 15 is not confirmed against NIC's current rule; it is configuration
evidence ewbNo, docNo, generatedAt and partAValidUntil (UTC), partAValidityDays, transMode, missing (VEHICLE or TRANSPORT_DOCUMENT), transporterId
Time ewb_dt is IST text in the same three formats as the validity; it is read as IST
Beside EWB_EXPIRY A bill with no Part-B and a validity about to end carries both. findings counts both; uniqueDocuments counts it once; GET /findings/{id} lists the other under companions
Needs to see it ewb.view
Kept 1 year (OPERATIONAL)

EWB_MISSING_FOR_IRN in detail

IRN_INVOICE Document type INV (configuration), latest e-invoice record SUCCESS / ACT / ACTIVE with no e-way bill number, no e-way bill row that is not cancelled, at least one goods line (a line flagged as a service does not count), and either the document declares transport (basis: DECLARED) or its value is at or above valueThreshold (50,000, basis: VALUE_THRESHOLD), acknowledged more than graceHours (4) and less than lookbackDays (7) ago
EWB_DOCUMENT An e-way-bill-only upload, or a posted delivery challan / bill of supply, whose latest attempt failed with NIC's refusal or with our own data check (LOCAL_VALIDATION) and that still has no e-way bill. A fault on our side (LOCAL_ERROR) is not listed. The failure must be within the look-back. Needs no grace period: the user has already tried
evidence (invoice) basis (DECLARED or VALUE_THRESHOLD: why goods are thought to be moving), irn, ackDt, lookbackEndsAt, goodsLines, totalLines, shipTo, valueThreshold; and ewbRefused when NIC has already refused an e-way bill for it
evidence (document) failedAt, lookbackEndsAt, outcome (NIC or LOCAL_VALIDATION), errors[] (up to three: code, message, help), errorCode, error, fixedBy
due_at null. This is a blocker, not a deadline
Needs to see it ewb.view
Needs to decide it attention.dispose and ewb.generate (Tax Controller, Logistics Manager)
Kept 1 year (OPERATIONAL). A decision is kept 8 years (COMPLIANCE) and the document is not raised again

An IRN alone never means "e-way bill required". The application has no rule that decides that; it only permits generating one once an IRN exists. This rule therefore needs a sign that goods are moving. A document that declares transport is the application's own signal: it is what makes the IRN request ask NIC for an e-way bill in the same call, so an IRN with such a declaration and no bill is a gap whatever its value. Without a declaration, the value is a weaker guess. A document with neither is not listed.

The value threshold is one number for everyone. E-way bill thresholds and exemptions vary by state and by goods, so some findings will not need a bill. The four decisions are how those are absorbed; a per-registration threshold is a later phase. The 50,000 figure, the 7-day look-back and the 4-hour grace period are configuration. They have not yet been confirmed against the current rules.

EWB_EXPIRY in detail

Window From extensionWindowHours (8) after expiry to horizonHours (24) before it. Both are configuration, not code
Time zone ewb_valid_till is stored as IST wall-clock text in three formats; it is read as IST. due_at is the expiry as a UTC instant
evidence ewbNo, docNo, status, validTill (UTC), validTillIst (as NIC shows it), extensionClosesAt (UTC), vehicleNo, fromPlace, toPincode, transporterId. A blank value is null
Countdown GET /findings returns hours_left (negative once expired) and overdue_hours. They are computed when you ask; the evidence does not carry them
Document When the bill was raised for a document in Complifly, the finding carries that document's number, customer and value
Not listed Bills expired more than 8 hours ago and never marked delivered (see below), bills with no readable validity (Part-A only), delivered and cancelled bills
Needs to see it ewb.view
Kept 1 year (OPERATIONAL). A lapse is kept 8 years (COMPLIANCE)

Expired bills that were never closed are not in this rule. On the development database 243 live bills are more than 8 hours past expiry; nearly all will be delivered goods nobody marked delivered. The Phase 1 EWB_EXPIRED alert counts them; this rule does not, so they will not appear in the Attention Center after the cutover. Mark them delivered (POST /api/ewb/{ewbNo}/mark-received) to keep the records tidy.

NIC's 8-hour extension window is the rule as we understand it, held as a parameter. It has not yet been confirmed against NIC's current advisory.

QUARANTINE_HELD in detail

Sources Excel: td_invoice_quarantine_header rows not Resolved, Archived or Purged (Ready For Reprocess is still held). ERP: td_ingest_rejection rows with status = OPEN and a GSTIN, except reason = DUPLICATE. ERP rejections with no GSTIN cannot be attributed to a registration and are not listed
Grace period Nothing is raised until the row has been held for graceHours (default 4, configuration), so nobody is alerted mid-correction. It is measured from when the row arrived (UTC)
One per document The newest row per document number per source. If that row is later closed and an older one for the same document is still open, the older one is raised
evidence (both) heldSince, errorCount, errors[] (first three: field, message), documentExists, and when the upload carries an IRN deadline: needsIrn, daysLeft, deadline
evidence (Excel) uploadJobId, uploadFile, uploadedBy, lifecycleStatus; each error also has excelRow. Errors come from the upload's validation log; a row with none falls back to its own summary text
evidence (ERP) sourceSystem, reason, repeatCount, externalRef, lastSeenAt
documentExists A document with the same number is already in Complifly. The row is still listed: a corrected re-upload blocked from replacing the original is held, and only the customer knows whether this is that or a leftover
duplicateDocNoOnly Present and true when "document number already exists" is the only error. Nothing to correct — only to discard. Such a hold is MEDIUM (configuration: duplicateOnlySeverity), never CRITICAL, so it stays visible and countable without competing with holds that can be fixed
due_at The IRN deadline when there is one; null otherwise
Needs to see it irn.view
Kept 1 year (OPERATIONAL)

Beside the deadline rules: an upload held for 28 days is both QUARANTINE_HELD (why it is stuck) and IRN_WINDOW (when it is due). findings counts both; uniqueDocuments counts it once. GET /findings/{id} lists the other open findings on the same upload under companions.

DOC_REJECTED_NIC in detail

In scope Invoices, credit notes and debit notes in location: REJECTED only (NIC refused them). Not e-way-bill-only documents, not B2C, and not e-way bill refusals — those belong to a later e-way bill rule
What counts as "the customer can fix it" The latest submission attempt is DATA_ERROR or STATUTORY. For documents refused before attempt tracking began (2026-08-20), the stored NIC code is classified the same way. Then refusals matching the rule's platformCodes / platformMessages (configuration, not code) are dropped, because NIC and the GSP report some platform failures as business errors — code 5002 is used both for a field validation and for "Application error", so the message decides
evidence nicErrors[] (up to 3: code, message, and help — { key, fixedBy, nextStep } when the Help Centre has an entry for that code, otherwise null), nicErrorCode / nicError (the first one), fixedBy (you, admin, statute or support, from the first code with help), outcome, attemptNo, attemptedAt, daysLeft, deadline
A second refusal The finding stays open with the new reason in evidence, and a CORRECTION_ATTEMPTED event is added to its timeline
Beside the deadline rules One document can have this finding and an IRN_WINDOW finding. findings counts both; uniqueDocuments counts the document once. GET /findings/{id} lists the other open findings on the document under companions
due_at The IRN deadline: 23:59:59 IST on the 30th day after the document date
Needs to see it irn.view
Needs to decide it attention.dispose and irn.generate
Kept 1 year (OPERATIONAL). A recorded WITHDRAWN decision is kept 8 years like every decision

IRN_WINDOW in detail

In scope As IRN_WINDOW_PASSED: invoices, credit notes and debit notes; not e-way-bill-only documents; not B2C
evidence docDate, deadline, daysLeft (0 on the last day), and what refused it, as for IRN_WINDOW_PASSED
due_at The deadline: 23:59:59 IST on the 30th day after the document date. hours_left in the list counts down to it
Severity Calendar days, IST — a document turns CRITICAL at the start of the day it has 3 days left, not at an hour boundary. A change is recorded as a SEVERITY_CHANGED event and sets escalated_at. Acknowledging or assigning does not stop the clock
After the deadline This finding closes EXPIRED / DEADLINE_PASSED, and the IRN_WINDOW_PASSED finding that replaces it carries this one's id in previous_finding_id — follow it to see the whole history of one document
Needs to see it irn.view
Kept 1 year (retention_class: OPERATIONAL). If the deadline is missed, the IRN_WINDOW_PASSED finding is the record kept for 8 years

Bulk actions on IRN_WINDOW: acknowledge, assign and unassign work as for any rule; dispose fails per item with INVALID_RESOLUTION_CODE and changes nothing.

IRN_WINDOW_PASSED in detail

In scope Invoices, credit notes and debit notes. Not e-way-bill-only documents, and not supplies to consumers (B2C)
location PENDING (never submitted), REJECTED (NIC refused it), QUARANTINE (failed upload checks), ERP_REJECTED (refused at the ERP API)
Not duplicated A quarantined or ERP-rejected copy of a document that already exists is not a second finding
evidence docDate, deadline, daysOverdue, and what refused it: nicErrorCode / nicError, or validationErrors[] + uploadFile, or erpErrors[] + sourceSystem
due_at The deadline: 23:59:59 IST on the 30th day after the document date
Needs to see it irn.view
Needs to decide it attention.dispose and irn.generate
Kept 8 years, however it closed (retention_class: COMPLIANCE)

The 30-day limit is NIC's. Complifly applies it to every registration with e-invoicing enabled; applying it per registration is planned.

GET /overview

Every registration the caller may see, for a screen that is not inside one registration (the Launchpad). The registrations come from the verified token, never from the request. Only registrations switched onto the new Attention Center (COMPLIANCE_INTELLIGENCE_V2) are counted; the others are listed as v2: false and add nothing to the totals, because their numbers are a different count. v2: null means the flag could not be read.

Field Meaning
registrations[] gstin, v2, and for a counted one uniqueDocuments, findings, valueAtRisk (null = unknown, never zero), severityTotals, worst, sweepHealthy, sweepReason, lastCheckedAt, and topGroups[] (three biggest: ruleKey, title, severity, uniqueDocuments, route). Most urgent first. A registration that failed to load has error: true; it does not fail the rest
totals registrationsCounted, registrationsNotOnNewAttentionCenter, registrationsUnavailable, registrationsNotChecking, and the sums of uniqueDocuments, findings, valueAtRisk, severityTotals over the counted registrations
truncated true when the caller has more than 100 registrations; the first 100 are shown

Each registration is read through the same summary a single-GSTIN caller gets, with the caller's own role, so a role sees no more here than on the screen.

Notification profiles

Nothing is emailed until two things are both true: Complifly has switched on email sending for the company (the NOTIFICATION_DISPATCH_ENABLED flag, default off, Super Admin only), and a registration or organization profile for that event is on. Profiles are saved either way. The in-app bell is a separate thing: it is on for everyone by default and is not affected by either.

A profile says who is told about a finding event. There is no administration screen of its own: a person edits theirs in Settings & Profile > My Notifications, a registration's in the Operational Controls GSTIN Overrides drawer, and an organization's under Operational Controls > Company Defaults. These endpoints are what those screens call.

Scope Path Whose profile Who may change it
user /user The caller's own. There is no id in this path: it is always the token's user Anyone signed in, for themselves. A personal profile can only switch the bell off, opt in or out of email, or raise its lowest severity. It cannot name recipients
gstin /gstin/{gstin} One registration An organization administrator, or a holder of admin.manage_users, inside their own company
org /org/{companyId} One organization An organization administrator

A Super Admin may reach any company, exactly as in Operational Controls. attention.act and admin.view are deliberately not the test: this is an administration setting.

The tenant boundary is the one Operational Controls enforces. A gstin or org in the path is checked against the token's own company before anything is read or written. A registration outside it is 403 GSTIN_NOT_PERMITTED (the same answer whether it belongs to someone else or does not exist), and another company is 403 COMPANY_NOT_PERMITTED. orgId and userId come only from the verified token, never from a header, query or body.

Which channel carries which event (the GET response lists this in channels, so a screen does not hardcode it):

Event Bell Email Fires when Told by default
ASSIGNED yes yes A finding is assigned (self-assignment sends nothing) The assignee
ESCALATED yes yes Severity rises The owner
REOPENED yes yes A decision is reopened Whoever recorded it (not whoever reopened it)
CRITICAL_CREATED yes no A critical finding is first found, in the last 24 hours Everyone who may open that kind of finding. Only a person can configure it, for themselves: a gstin or org profile for it is 400 EVENT_HAS_NO_EMAIL
DAILY_SUMMARY, WEEKLY_SUMMARY no yes At sendHourLocal (and sendWeekday for weekly) in timezone (default Asia/Kolkata) Recipients only

A channel an event does not have is stored off whatever the body says. OVERDUE, which an earlier design had, is not an event: it is 400 INVALID_EVENT.

Governance is Platform → Organization → Registration → User, and an explicit Off above is never bypassed. The company's NOTIFICATION_DISPATCH_ENABLED is the platform level. Then:

If this is Off Result
The organization's profile for the event (a row with enabled: false, or channelEmail: false) Nobody is emailed on any registration of the company. A registration cannot turn it back on: saving one On is 409 BLOCKED_BY_ORGANIZATION, and the registration GET lists the blocked events in orgOff
The registration's own profile Nobody is emailed on that registration, even where the organization is On
A person's own profile (channelEmail: false or enabled: false) That person is not emailed. Always works

A profile that does not exist is "not set up", not Off: a registration may turn on what the organization never set up, and a person may opt in. A registration may always be stricter than the organization (saving it Off is always allowed). When nothing above is Off, the registration's own profile is used for that event, else the organization's; a person's channelEmail: true opts in for what is theirs, and null means no opinion, follow the registration or organization (only a person may send null; the other scopes store true). A person's choice can narrow, never bypass. The in-app bell is outside this hierarchy: it is set per person only.

GET returns { scope, scopeId, canEdit, profiles[], users[], events[], emailEvents[], summaryEvents[], channels, defaults, limits, roles[], canAddEmail, emailConfigured, dispatchEnabled, orgOff[] } (orgOff is the events the organization has Off, and is only filled for a gstin read). dispatchEnabled is the company's kill switch: show it above the settings, because "nothing arrived" has this one obvious cause.

PUT body: enabled, channelEmail, channelInapp, minSeverity (CRITICAL, HIGH, MEDIUM or null), sendHourLocal (0–23), sendWeekday (1 Monday to 7), timezone, sendWhenEmpty, recipients[] ({ "type": "USER" | "ROLE" | "EMAIL", "value": … }, gstin and org only, at most 50) and rowVersion (or an If-Match header). A stale rowVersion is 409 STALE_PROFILE. Recipients are replaced as a set; an address already confirmed stays confirmed. The response is { profile, confirmationsPending, confirmationsSent, confirmationsHeld }.

Who is told, and how:

Rule Detail
Roles resolve at send time A ROLE recipient becomes the active users who hold that role on that registration and may open that kind of finding when the email is sent. Someone given the role later is included without editing the profile, and a role added to the product later is addressable with no change here
Email addresses Only an organization administrator can add one. It receives nothing until its owner follows the confirmation link (valid 72 hours, once). The mail carries counts, severities and links, never a customer name, a document number or a value; every one has an unsubscribe link
The kill switch also holds the confirmation mail While NOTIFICATION_DISPATCH_ENABLED is off for the company, saving an address returns confirmationsHeld: true and sends no confirmation, and resend sends none. Switch it on, then resend
Content One email per recipient per event per check: up to 10 items, then a count
Delivery Event emails are sent at most once: a ledger row is written before the send, and a failed send is recorded and not retried. A failed summary is retried within its period. A summary with nothing to report is not sent unless sendWhenEmpty, except that a summary for a registration whose checks are not running always says so
Only on the new Attention Center A registration not on COMPLIANCE_INTELLIGENCE_V2 is never emailed: the links would open the earlier screen
Server switch ATTENTION_NOTIFICATIONS_ENABLED on the server (see Environment Variables) stops the job for every company. Off: nothing sends, anywhere. The flag above is the per-company switch inside it
Audit Every save and removal is written to the audit log (ATTENTION_NOTIFICATION_PROFILE_SET, ATTENTION_NOTIFICATION_PROFILE_REMOVED) against the company, with an address shown only as a…@example.com. It is not written to the Operational Controls change history, which is for controls

Errors (all { code, message }):

HTTP code When Fixed by
400 INVALID_EVENT, INVALID_SEVERITY, INVALID_HOUR, INVALID_WEEKDAY, INVALID_TIMEZONE, INVALID_GSTIN, INVALID_COMPANY A value is not one the API accepts you
400 EVENT_HAS_NO_EMAIL A gstin or org profile for a bell-only event you
400 USER_PROFILE_HAS_NO_RECIPIENTS Recipients were sent with the user scope you
400 / 422 TOO_MANY_RECIPIENTS, INVALID_RECIPIENT_TYPE, INVALID_ROLE, INVALID_EMAIL A recipient is not valid you
401 UNAUTHENTICATED No usable token sign in again
403 SCOPE_NOT_PERMITTED The caller's role may not edit that scope admin
403 GSTIN_NOT_PERMITTED, COMPANY_NOT_PERMITTED The registration or company is outside the caller's own admin
403 EMAIL_RECIPIENT_NEEDS_ORG_ADMIN An address was added by someone who is not an organization administrator admin
404 NOT_PENDING resend for an address that is already confirmed or not on the profile you
404 UNKNOWN_COMPANY An org path names a company that is not in the company catalogue admin
404 UNKNOWN_GSTIN A gstin path names a registration that belongs to no company. Refused for a Super Admin too: a setting for it would belong to nobody admin
409 STALE_PROFILE rowVersion did not match you, reload and retry
409 BLOCKED_BY_ORGANIZATION A gstin profile was saved On for an event the organization has switched Off admin: switch it on for the organization first
422 RECIPIENT_NOT_ELIGIBLE A named user is not an active user of this registration or organization, or has no email address you

The two links with no login look the secret up by its hash (confirm) or exact value (unsubscribe), work once, and show a plain page that never repeats the token or the address.

Lifecycle

Status Set by Meaning
OPEN Complifly True now; nobody owns it
ACKNOWLEDGED A user Someone owns it. Still true; deadlines still run
RESOLVED A user, with a decision Closed by a recorded decision. Not raised again for that document
AUTO_CLOSED Complifly Re-checked and no longer true — the document was fixed
EXPIRED Complifly No longer actionable — deadline passed, document discarded, or the check does not apply to the GSTIN

close_reason says why a finding closed (IRN_GENERATED, RESUBMITTED, DEADLINE_PASSED, DISPOSITION, CONDITION_CLEARED, …). retention_class says how long the closed finding is kept: COMPLIANCE (8 years — decisions and lapsed statutory deadlines) or OPERATIONAL (1 year).

GET /summary

Optional ?domain=EINVOICE|EWAYBILL|INGEST|INTEGRATION. A domain narrows what is counted, not only what is listed: findings, uniqueDocuments, parties, valueAtRisk and severityTotals then describe that domain only, and the response carries domain.

{
  "gstin": "29ABCDE1234F1Z5",
  "findings": 35,
  "uniqueDocuments": 21,
  "parties": 9,
  "valueAtRisk": 4120000,
  "severityTotals": { "CRITICAL": 3, "HIGH": 30, "MEDIUM": 2, "LOW": 0 },
  "lastCheckedAt": "2026-10-19T05:12:04.000Z",
  "sweepHealthy": true,
  "sweepReason": null,
  "groups": [ {
    "ruleKey": "IRN_WINDOW", "domain": "EINVOICE", "kind": "DEADLINE",
    "severity": "CRITICAL", "counts": { "CRITICAL": 3, "HIGH": 14, "MEDIUM": 0, "LOW": 0 },
    "total": 17, "uniqueDocuments": 17, "parties": 5, "owned": 5, "overdue": 0,
    "earliestDueAt": "2026-10-04T18:29:59.000Z", "oldestDetectedAt": "2026-09-27T04:10:00.000Z",
    "valueAtRisk": 1240000,
    "title": "…", "reason": "…", "impact": "…",
    "action": { "label": "…", "route": "/einvoice?attention=IRN_WINDOW" },
    "canAct": true, "canDispose": false, "hasDispositions": false
  } ]
}
Field Meaning
findings vs uniqueDocuments A document flagged by two rules is two findings and one document. Report uniqueDocuments as "how many documents need attention"
valueAtRisk (top level) Counted once per document. A document on two cards adds its value to each card but to the total only once, so the total is lower than the sum of the cards. null means no value is known, never zero
groups[].valueAtRisk The value of that card's documents. Some rules have none (e-way bills not linked to a document, ERP updates): null
groups[] order Severity, then earliest due date, then value at risk
groups[].severity The worst severity in the group; counts gives the split
owned Findings acknowledged or assigned
sweepReason When sweepHealthy is false: SWEEP_DISABLED, NEVER_RUN, STALE (no completed check in two intervals) or LAST_RUN_FAILED
canAct / canDispose What the calling user may do on this kind of finding

Cached for up to 60 seconds per GSTIN; a lifecycle action or a completed check clears it.

GET /rules

One entry per rule the caller can see: ruleKey, title, domain, kind, helpKey, action (label, route), canAct, canDispose, hasDispositions, entityTypes, severityBasis (fixed: one severity; rule: decided per finding), retentionClassDefault, evalOrder, and resolutions — each with code, label, noteRequired and the evidence fields it needs (field, label, pattern), enough to build a decision form without another call.

GET /findings and GET /history

/findings returns open findings by default; /history returns closed ones.

Query Values Default
rule A ruleKey all visible rules
status active (open + acknowledged), closed, or one status active (closed on /history)
severity CRITICAL, HIGH, MEDIUM, LOW all
assignee me, none, or a user id all
location Where the document is stuck: PENDING, REJECTED, QUARANTINE, ERP_REJECTED, IRN_INVOICE, EWB_DOCUMENT, ERP_WRITEBACK. The e-way bill rules EWB_EXPIRY and EWB_PART_B_PENDING have none (null) all
closeReason, resolutionCode, retentionClass exact value all
from, to ISO date-times — detection time on /findings, closing time on /history none
q Text in document number, party name or GSTIN, e-way bill number none
sort priority, due_at, value, age, closed_at priority (severity, due date, value)
page, size size up to 200 1, 50
idsOnly 1: return only { total, limit, truncated, ids[] } for everything matching the filter, up to 500, for a bulk action. truncated: true means more match than one bulk call accepts; act in batches off
format csv: a file of up to 50,000 rows. /history returns the closed items, /findings the open ones, with the same filters. The last two columns are Due (UTC) and Where JSON

Each item carries the finding (finding_id, rule_key, doc_no, doc_date, party_name, doc_value, severity, status, due_at, evidence, assignee_user_id, lifecycle_version, …), four ageing figures and companions — other open findings on the same document.

Ageing field Meaning
age_hours Since the finding was first detected
hours_left Until due_at (negative once overdue)
overdue_hours Past due_at; null if not overdue
escalation_age_hours Since severity last rose; null if it never has

Lifecycle calls

All take a JSON body and return { "finding": {…}, "unchanged": false }.

Call Body Notes
acknowledge { "note"? } Acknowledging an already-acknowledged finding succeeds with unchanged: true
assign { "userId", "note"? } userId must come from GET /assignees
unassign —
notes { "note" } Up to 1,000 characters
dispose { "resolutionCode", "evidence"?, "note"? } resolutionCode must be one that GET /rules lists for the finding's rule; some need evidence fields or a note
reopen { "note" } Opens a new finding linked to the old one; the decision stays on record

Concurrency. Send the lifecycle_version you last read as an If-Match header. If the finding changed since — a colleague acted, severity rose, or it closed — the call fails with 409 STALE_FINDING. Omit the header to act regardless. Routine re-checks do not change lifecycle_version.

Bulk. POST /findings/bulk with { "action": "acknowledge" | "assign" | "unassign" | "dispose", "findingIds": [ … ], … } and the same fields as the single call. Each finding succeeds or fails on its own; the response lists every outcome:

{ "action": "acknowledge", "requested": 3, "succeeded": 2, "failed": 1,
  "results": [ { "findingId": "41", "ok": true, "status": "ACKNOWLEDGED" },
               { "findingId": "44", "ok": false, "code": "INVALID_TRANSITION", "message": "…" } ] }

Errors

HTTP code When Fixed by
400 GSTIN_REQUIRED No single GSTIN selected (x-gstin missing or all) you
400 INVALID_RESOLUTION_CODE The decision is not one this kind of finding allows — or it allows none you
400 INVALID_TRANSITION Bulk call with an unknown action, no ids, or more than 500 ids you
401 TOKEN_MISSING No token you
403 — The role lacks attention.act / attention.dispose admin
403 RULE_NOT_PERMITTED The role holds the permission but not the document right for this finding admin
404 FINDING_NOT_FOUND No such finding for this GSTIN, or the caller cannot see that kind you
409 INVALID_TRANSITION The action does not fit the finding's status (e.g. acting on a closed finding) you
409 STALE_FINDING If-Match did not match you — re-read and retry
409 NO_RULES_ENABLED POST /evaluate with no rules enabled support
422 EVIDENCE_REQUIRED A required evidence field or note is missing or malformed you
422 ASSIGNEE_NOT_ELIGIBLE The user cannot be assigned findings on this GSTIN or of this kind you
429 EVALUATE_RATE_LIMITED POST /evaluate twice within a minute you — wait
500 ATTENTION_ERROR Unexpected failure; nothing was changed support

A finding belonging to another GSTIN is always 404, never 403: the API does not reveal whether it exists.

Validation

  • GET /summary for a GSTIN with rules enabled: sweepHealthy is true and lastCheckedAt is within the last two check intervals.
  • For any group, GET /findings?rule=<ruleKey> returns total equal to the group's total.
  • Acknowledge a finding, then GET /findings/{id}: the timeline's last event is ACKNOWLEDGED with your user id.

Troubleshooting

Symptom Cause Action
groups is empty and sweepReason is SWEEP_DISABLED The scheduled check is off on this installation (COMPLIANCE_SWEEP_ENABLED) An operator enables it — see Environment Variables. Requires server access
sweepReason is STALE No check has completed for two intervals An operator checks the application log for [ATTENTION] errors and the Observability dashboards for ATTENTION_SWEEP_FAILURE
A finding will not close after the document was fixed It closes at the next check (default every 15 minutes), or within seconds when event-driven checks are enabled Wait for the next check, or POST /evaluate
dispose returns INVALID_RESOLUTION_CODE for every code That kind of finding allows no decisions — it closes only when the document is fixed Fix the document
A user sees fewer groups than a colleague Groups follow document permissions Compare roles — see Roles and Permissions