Fiscal Entity Revenue
Revenue resume per fiscal entity — one entry per seller fiscal entity of the business, with aggregated totals, per-FE status, projected revenue status, and lists of errored/processing months. Backed by a monthly-refreshed cache fetched from the municipal portal via a thin HTTPS provider. The same data drives the revenue area of the MEI Diagnosis.
Big picture
Revenue is tracked per CNPJ / per fiscal entity. The endpoint returns one fiscal-entity-revenue-resume per seller fiscal entity of the business. Each resume aggregates the underlying monthly revenue records into a summary with api_status (errored/processing/succeeded), total_revenue_status (ok/nok — whether actual revenue exceeded the annual limit), projected_revenue_status (ok/warning_low/warning_high — forward projection with configurable thresholds), and lists of errored_months and processing_months. Fiscal entities whose owner professional is inactive are still listed with their stored data but are not refreshed.
The municipal portal is reached indirectly: an external revenue provider wraps the actual government portal and exposes a stable endpoint. The provider authenticates via an API key and signs into the portal with the fiscal entity's stored credentials.
Endpoints
| Method | Path | Purpose |
|---|---|---|
| GET | /professional-fiscal-entities/revenue | Revenue resumes per fiscal entity, scoped to a business. One entry per seller fiscal entity. The api_status reflects the aggregated status of underlying months. Triggers a background refresh for entities with an active owner. |
| GET | /businesses/{businessId}/revenue | Same shape as /professional-fiscal-entities/revenue, but scoped via the URL path. |
| GET | /fiscal-entities/revenue | Same shape as the business-scoped variant, but the business is taken from the ilm-business header. Use this from internal services. |
| GET | /fiscal-entities/{fiscalentityId}/revenue | Monthly revenue records for a single fiscal entity. Triggers a refresh if records are missing, errored, or from a previous month. |
| PATCH | /fiscal-entities/{fiscalentityId}/revenue | Force-refresh the monthly records for a single fiscal entity. Only updates months that are missing or errored — already-succeeded months are never overwritten. |
Data model
Revenue is stored per month in a dedicated table (not in the JSONB FiscalEntityMEI columns like the other diagnosis areas):
accounting_fiscal_entity_revenues
├── fiscal_entity_id → SellerFiscalEntity.Id
├── year "2026"
├── month "01" … "12"
├── amount_cents int64
├── invoice_count int
├── status requested | succeeded | errored
├── status_reason text (nullable, e.g. "certificate download failed: ...")
└── data JSONB
├── fiscal_month "MM/YYYY" (raw provider format)
├── amount_cents (raw, for audit)
├── invoice_count (raw, for audit)
└── revenue_job_id string ← only set while a lote job is in flight
The data.revenue_job_id field is set on every requested record when a lote (batch) refresh is submitted. The singleton poller clears the field once the job completes. It is purely an internal marker — clients should ignore it. The provider's raw payload fields (fiscal_month, amount_cents, invoice_count) are stored for audit and debugging.
The partner-scoped list returns one resume per fiscal entity (deduplicated). If a fiscal entity has multiple active owners, only one resume is returned. Use the GET /fiscal-entities/{id}/revenue endpoint to inspect individual monthly records for a specific fiscal entity.
Authentication
The revenue provider accepts two authentication methods, both ways the underlying municipal portal supports. Every call must carry an API key — without it the upstream returns 401 and records are marked errored.
1. NFse portal username + password
The fiscal entity must have pass_invoices credentials stored (created via POST /fiscal-entities/{id}/pass-invoices). The password is encrypted at rest and decrypted only at the moment of the call.
2. Digital certificate
Preferred when available, because it does not depend on a captcha-prone portal login. The certificate and password are stored in the accounting_fiscal_entity_certificates table. The .pfx file is on S3; the password is encrypted at rest and decrypted only at call time.
Validation
The system enforces that at least one of (certificate, pass-invoices) is available. If neither is present, all 12 months are marked errored with code: "no_credentials_available" and the response returns the professional with annual_revenue_centavos: null rather than failing the request.
Update & check flow
There are two update triggers with different behaviours:
PATCH (manual refresh, synchronous)
Always triggers an update but only touches months whose current status is missing (no record) or errored. Successfully-fetched months are never overwritten by PATCH. Each year's data is fetched with a synchronous POST /consultar against the revenue provider.
GET (smart refresh)
For a single fiscal entity (GET /fiscal-entities/{id}/revenue):
- Stuck
requestedrecords (older than 5 minutes) are first converted toerroredso they don't block refresh. - A GET then triggers an update if:
- No records exist — all months are missing, fetch everything up to the previous month.
- Any month is errored — retry the errored months only.
- All months are succeeded but the last update was in a previous month — this is a monthly refresh. Records that were fetched before their month ended (e.g. June updated on June 23) are considered premature and are reset to
requestedso they get re-fetched with final data. Months updated after the month ended (e.g. June updated on July 1) are final and are never touched again.
If all months are succeeded and the last update was in the current month, no refresh is triggered.
For the batch list (GET /professional-fiscal-entities/revenue and its aliases):
- For each fiscal entity that needs refresh, the service sets months to
requestedsynchronously. - It then submits one async lote (batch) request per entity to
POST /consultar/lotewith one entry per year covered. The provider responds immediately (HTTP 202) with ajob_idper entry. - Each
job_idis stored in the record'sdataJSONB asrevenue_job_id. - The service returns the (possibly stale) resumes to the caller right away.
- A singleton poller goroutine — one per process — picks up the new jobs from a Go channel and polls
GET /jobs/{job_id}every 1 second while there are active jobs. The poller blocks on the channel when the set is empty (no CPU, no polling). - On
concluido: the poller writes the per-month revenue into the records (skipping already-succeeded months with identical data) and clearsrevenue_job_id. - On
erro: the poller marks the records aserroredwith the upstream error message. - On
pendente/processando: the poller keeps waiting.
Why batch list is fire-and-forget
Without the lote API, each entity triggered its own goroutine making a synchronous POST /consultar — fine for a few entities, expensive for a business with 50+ seller FEs. The lote API hands the queue to the provider, which already throttles between batches. The platform only needs to track the resulting job_ids and write results back to the DB when each job completes.
Startup recovery
If the pod restarts mid-refresh, the singleton poller loses its in-memory active set. On startup it scans the DB for requested records that don't carry a revenue_job_id and marks them as errored with reason orphaned_on_restart — the next request retries them cleanly. Records that do have a revenue_job_id are left alone (the external API likely still has the job; the periodic 5-minute requested_timeout sweep will clean them up if the upstream never returns).
Month range
The current month is always excluded — it is not finalized yet. For example, on July 15, only months January through June are fetched. The current month's slot (July) is not created until the next month (August 1+), when July becomes the "previous month" and gets its final fetch.
Thresholds
The annual limit depends on the fiscal entity type. MEI entities use the MEI annual revenue limit — currently R$ 81.000,00 (8.100.000 centavos) for 2023, 2024 and 2025. Future years default to the most recent known value. The revenue is summed across the current calendar year.
Non-MEI entities use a configurable annual limit (default: R$ 4.800.000,00, the Simples Nacional EPP ceiling). The revenue is computed as the trailing 12 months from the latest available month, not a calendar year.
The resume has two status fields, split by calculation method:
total_revenue_status— compares actual total revenue against the annual limit."ok"if total ≤ limit,"nok"if total > limit.projected_revenue_status— forward-looking projection using two configurable thresholds. The system computesavg_month_revenue_centsfrom succeeded months, multiplies it by the remaining months in the year, and adds it to the current total. If the projected total is below the ok threshold, status is"ok". Between ok and warning thresholds:"warning_low". At or above the warning threshold:"warning_high".
JSON:API shape
{
"data": [
{
"type": "fiscal-entity-revenue-resumes",
"id": "fe-456",
"attributes": {
"calculation-type": "yearly",
"initial-month": "202601",
"last-month": "202606",
"api-status": "errored",
"api-status-reason": "no_credentials_available",
"total-revenue-status": "nok",
"projected-revenue-status": "warning_high",
"is-mei": true,
"total-revenue-cents": 4500000,
"avg-month-revenue-cents": 750000,
"invoice-count": 12,
"total-limit-cents": 8100000,
"total-limit-percentage": 5556,
"errored-months": ["202603"],
"processing-months": []
},
"relationships": {
"fiscal-entity": { "data": { "type": "fiscal-entities", "id": "fe-456" } }
}
}
]
}
The resume aggregates all monthly records for the fiscal entity within the period (yearly for MEI, trailing 12 months for non-MEI). The api-status reflects the highest-priority status among monthly records. total-revenue-status compares actual revenue against the limit; projected-revenue-status projects forward using configurable thresholds. total-limit-percentage is an integer representing hundredths of a percent (5556 = 55.56%).
Attribute values
| Attribute | Type | Possible values / format |
|---|---|---|
calculation-type | string | "yearly" (MEI, calendar-year sum) or "trailing" (non-MEI, rolling 12 months) |
initial-month | string | YYYYMM — e.g. "202601". Yearly MEI: January of the requested year. Trailing non-MEI: earliest of the 12-month window. |
last-month | string | YYYYMM — e.g. "202606". Always the previous calendar month; the current month is never included. |
api_status | string | "succeeded", "processing", or "errored" (priority: errored > processing > succeeded) |
api-status-reason | string | Free text surfaced from the first errored underlying record. Common values: "no_credentials_available", "certificate_expired", "server_error", "forbidden", "unauthorized", "not_found", "unprocessable", "api_error", "request_failed", "decrypt_error", "parse_error", "unknown_error" |
total-revenue-status | string | "ok" or "nok" — whether actual total revenue exceeds the annual limit. |
projected-revenue-status | string | "ok" (projected below the lower threshold), "warning_low" (projected between the two thresholds), "warning_high" (projected at or above the upper threshold). Default thresholds: R$ 72k / R$ 81k. |
is-mei | bool | true or false |
total-revenue-cents | int64 | Sum of succeeded month amount_cents in the period |
avg-month-revenue-cents | int64 | total_revenue_cents / succeeded_count. 0 when there are no succeeded months. |
invoice-count | int | Sum of succeeded month invoice_count in the period |
total-limit-cents | int64 | MEI: R$81k (8_100_000). Non-MEI: R$4.8M default (480_000_000), configurable. |
total-limit-percentage | int | Integer in hundredths of a percent. 5556 = 55.56%. 10000 = 100%. 0 when no revenue. |
errored-months | string[] | Array of YYYYMM — months whose underlying record has status=errored |
processing-months | string[] | Array of YYYYMM — months whose underlying record has status=requested (refresh in flight) |
Gotchas
- First request is slow — the first call for a year with no cached records triggers 12 months of scraping. Subsequent calls return within 200ms because the cache is fresh.
- Monthly refresh is lazy — the monthly refresh only triggers when someone actually calls the endpoint. The first call in a new month is slower because it re-fetches prematurely-fetched months.
- PATCH never overwrites succeeded — if a month is already
succeeded, PATCH will not re-fetch it even if the data has changed. Use the monthly refresh (GET) to correct prematurely-succeeded months. - Inactive-owner entities are listed but not refreshed — fiscal entities whose owner professional is inactive appear in the response with their existing stored data, but no background refresh is triggered for them.
- One resume per fiscal entity — the list is deduplicated by fiscal entity ID. If a fiscal entity has multiple owners, only one resume is returned.
- Only one provider is currently supported — the system is designed to support multiple providers, but only one is currently implemented.
- No retry on transient provider errors — if a provider call fails, all 12 months of the affected year are marked
errored, and the next request retries them (since errored months always trigger a refresh).