Skip to content

Data semantics

The reference tells you the shape of every response. This page tells you what the values mean. Everything here is a place where a technically successful request can still give you a number you did not intend to use, so it is worth reading once before you write the integration rather than after.

1. Books do not start at the beginning of time

Section titled “1. Books do not start at the beginning of time”

Every record from GET /business_entities carries two dates that bound what you can ask for and what you can trust:

  • books_start_date — the earliest date its books cover. null where the entity has no books yet (reporting_available: false).
  • books_closed_through — the most recent date a bookkeeper has finalized them through. null until the first month is closed, which includes a live entity whose books simply have not been closed yet — read it as “nothing is settled”, not as “no books”. §3 covers what it is for.

Requested ranges are clamped to books_start_date. If you ask for a trailing twelve months and the entity’s books begin four months ago, you get four months of data, and the response tells you so: start_date and end_date in the payload are the effective range we reported on, not the range you asked for.

This is not an edge case. An entity typically arrives on Flychain part-way through its financial year, and its books begin when we start keeping them, so a short window is the normal early state of a new entity rather than a fault. The failure to design against is treating those months as missing or as zero revenue: a partial year of books is “the books start here”, and a per-month figure of zero for a month before books_start_date would be a fabrication, not a fact.

Two consequences worth building in:

  • Compare the returned range against the range you asked for, and record which one the figure covers. Any downstream calculation that assumes twelve months because it asked for twelve months will be wrong for months at a time.
  • A range entirely before books_start_date is a 400, not an empty statement. We would rather fail your request than hand you a zero that looks like a real measurement. The same applies to a range entirely in the future.

end_date is also capped at today, which is the ordinary case for a job that asks for the current month every day.

The balance sheet clamps the other way, and on purpose. An as_of_date earlier than books_start_date is raised to it rather than refused, and the response’s as_of_date tells you which date you got. The difference is what the two reports are: a range with no books behind it is not a period, so there is nothing to return but a fabricated zero — but a balance sheet at the date an entity’s books begin is its real opening position. Compare as_of_date against the date you asked for, exactly as you would compare a range.

The monthly balance sheet is clamped the same way, by column count rather than by date. Its optional months parameter (default 12, maximum 36) counts the as_of column, so months=12 asks for eleven prior month-ends plus the as-of — and an entity with four months of books returns four columns however many you ask for. periods is empty when the as-of date falls in the entity’s first month of books: there are no complete prior months to report, and as_of is the only column. An empty periods is that state, not a failure.

2. is_closed and is_complete are different questions

Section titled “2. is_closed and is_complete are different questions”

Both flags appear on every period of a monthly response — income statement, balance sheet and cash flow report alike. is_closed appears on its own wherever a response covers one point or range rather than a series: the single-range and single-date endpoints, and whole_period / as_of inside a monthly one.

On a monthly balance sheet, is_complete is always true — its periods are month-end columns, and the one column that can be mid-month is as_of. It is returned anyway so a period reads the same on every report.

Flag Question it answers
is_complete Has this period’s end date passed? false for a period ending today or later — in practice, the month in progress.
is_closed Has a Flychain bookkeeper finalized the books through this period’s end date?

is_complete is a calendar fact. is_closed is an accounting fact, and it is the one that tells you whether a figure can still move.

is_closed is computed from the entity’s books_closed_through date, returned on every record from GET /business_entities: it is true for any period ending on or before that date, and false for every period when that field is null. The two are the same fact in two forms, which is useful — one discovery call tells you which periods across your whole roster are settled, without pulling a report per entity to find out.

A note on is_complete: it is about the period, not the calendar month. If your requested range starts or ends mid-month, the first and last periods are clipped to your range (see §5), and a clipped period reads is_complete: true once its end date has passed even though the calendar month it is labelled with is not fully covered.

Our bookkeepers aim to close each month within the first couple of weeks of the following month. Treat that as a target, not a guarantee — a close can land later, and a process that assumes a fixed date will occasionally read provisional figures as final. We are telling you this rather than promising a date we might miss, because you are invoicing on these numbers.

is_closed is the authoritative signal, and it is why the flag exists. Before a close, figures are provisional: categorization is largely automated, so revenue is usually close to final well before the books close, but as in any accounting system the item most likely to move a figure is a large non-revenue deposit — a loan, say — that has not yet been reconciled. That can make revenue read high and then settle lower.

The pattern that handles this deterministically:

  1. Re-pull trailing months on a schedule (the monthly endpoints make this one request).
  2. Treat every period with is_closed: true as settled, and stop re-pulling it.
  3. Treat every period with is_closed: false as provisional, and expect it to change.
  4. If a figure you have already acted on changes after close, true it up from the closed value.

If you would rather only ever act on final figures, wait for is_closed: true on the period before using it. That is a choice on your side; nothing changes on ours.

Each entity’s basis — CASH or ACCRUAL — is recorded against its books and is the basis its financials are prepared and reviewed on. We return accounting_basis on every entity record and every report, so a figure is never ambiguous about which basis it is on.

There is no per-request basis switch, and adding one would be misleading rather than helpful: a report is produced on the basis the underlying books are kept on, and re-expressing cash-basis books on an accrual basis is a bookkeeping exercise, not a query parameter. Where an entity needs to change basis, that is a decision made with them and the field will reflect it — it will not change without notice.

Practically: an entity’s basis is stable, but do not assume every entity in your relationship shares one. Store the basis alongside any figure you keep.

  • All dates are YYYY-MM-DD, zero-padded. 2026-7-1 is rejected, as is a date that does not exist (2026-02-31). Both are 400 INVALID_REQUEST.
  • Ranges are inclusive of both endpoints. start_date=2026-07-01&end_date=2026-07-31 is the whole of July.
  • Not every report takes a range. The income statement and the cash flow report do — start_date and end_date, both required. The balance sheet takes a single required as_of_date, because a balance sheet is a position on a date rather than activity over a window; balances accrue from the entity’s inception, so there is nothing for them to accumulate over. Its monthly variants are sized by months rather than by a range (see §1), which is why their periods carry an end_date and no start_date.
  • Report windows are evaluated in UTC. Do not send a local-timezone-adjusted date; a shifted window can pull a transaction from the neighbouring day into the wrong period.
  • generated_at is ISO 8601 UTC. Reports are computed on request, so it is also the as-of time of the figures. There is no caching delay to work around: an on-demand pull reflects the current state of the books.
  • Monthly periods are clipped, not extended. A range from the 15th to the 14th yields a first period starting on the 15th and a last period ending on the 14th. The label (“July 2026”) is derived from the period’s start date, so use the dates, not the label, to know what a period covers. Align your ranges to month boundaries if you want whole calendar months.
  • whole_period is computed independently, over the full range, rather than summed from the per-month figures. The two agree; a discrepancy would indicate an upstream inconsistency rather than rounding. On a cash flow report, agree means the three flow figures sum across periods while the two balances do not — starting_cash_balance_cents is the first period’s and ending_cash_balance_cents the last period’s, because a balance is a position rather than an amount that accumulated.
  • A clipped period’s cash balances follow the clip. On a monthly cash flow report, a first period starting on the 15th has the balance on the 15th as its starting_cash_balance_cents, not the balance on the 1st.

6. Entities that are not reportable, and entities that are gone

Section titled “6. Entities that are not reportable, and entities that are gone”

Two distinct situations, reported differently on purpose.

Not yet reportable, or no longer reportable. GET /business_entities lists the entity with reporting_available: false. This covers both directions: an entity still being onboarded, and one that has been deactivated. A report request for it returns 409 BOOKS_NOT_AVAILABLE. Skip these rather than recording a zero — an entity mid-onboarding would otherwise read as a business with no revenue. This is the same for both kinds of key.

Outside your key’s reach, and here the two audiences are answered differently — on purpose, because they need different things.

With a partner key: the entity or provider stops appearing in the discovery endpoints, and a report request returns 403, with a code (PROVIDER_NOT_IN_PARTNER_SCOPE or BUSINESS_ENTITY_NOT_IN_PARTNER_SCOPE) distinct from the 404 you get for an id that never existed. That distinction is deliberate: a provider leaving the relationship is a thing your platform should be able to detect and flag, rather than discovering it as a silent absence.

So for a partner: PROVIDER_NOT_IN_PARTNER_SCOPE and BUSINESS_ENTITY_NOT_IN_PARTNER_SCOPE mean “this existed and is no longer yours — stop querying it and tell someone”. 404 means “this id is wrong”.

With a provider key: an id that is not yours returns 404 — PROVIDER_NOT_FOUND or BUSINESS_ENTITY_NOT_FOUND, the identical answer an id that never existed gets. There is no 403 scope code on this side, and no way to tell the two apart. You hold exactly one valid provider_id and GET /business_entities already lists your entities, so there is nothing here you need to distinguish — and confirming that some other provider’s id is real is not something we will do.

Match on the code, not on the status. Two other things arrive as 403 and mean something else entirely:

  • PARTNER_API_NOT_ENABLED (partner keys) and PROVIDER_NOT_ACTIVE / PROVIDER_API_NOT_ENABLED (provider keys) are about your own account, not about a resource. Every id you hold is still valid and nothing has left your reach. Treating one of these as a departure would flag your whole roster as churned.
  • ENDPOINT_NOT_AVAILABLE (partner keys) is about the endpoint, not the resource: the balance sheet and cash flow families are provider-key only. The entity is fine and your URL is fine — see which endpoints each key reaches.

Every monetary value is a signed integer in cents, USD. We hold monetary values in cents internally to avoid floating-point rounding in financial calculations, and expose them the same way so no precision is lost in transit. Expense categories are positive magnitudes, as they appear on a statement.

In the full statement, read total_amount_cents for a category or account total — it includes every account beneath that node. amount_cents is only what is posted directly at that node, which for a category root is usually zero but is not guaranteed to be. Reading amount_cents on a category is the single easiest way to under-report a figure.

The cash flow report is the exception, and it fails quietly. Its nodes are cash accounts rather than statement categories, and on a ledger total_amount_cents is always 0 — the figure lives in total_net_cash_flow_cents. A reader that applies the rule above to a cash flow ledger gets a zero rather than an error. One level down, on the individual transactions beneath a ledger, amount_cents is the amount that moved.

And on the cash flow report, reading one account and summing every account are different fields. ledgers is a flat list: where one account rolls up into another, both appear in it as their own entries. So total_net_cash_flow_cents — which includes everything beneath an account — is what to read for a single account and double-counts under a sum.

To aggregate, add up each account’s total_inflow_cents and total_outflow_cents. Those are the per-account figures the report totals are built from, so they reconcile exactly. net_cash_flow_cents is that account’s own net, useful for reading one row next to its rolled-up figure, but it reaches us as a separate value rather than as the sum of the two above — so use the pair when the arithmetic has to hold.

That extra level is itself worth knowing about: the cash flow full variants descend to individual transactions — line_id, datetime and the description as it reaches us — where the income statement and balance sheet stop at account level. It is why those payloads grow with an entity’s transaction volume, and why the summary variant is the one to point a recurring pull at.

Cash outflows carry their sign. total_outflow_cents is negative (or 0 where nothing left in the range), so total_inflow_cents + total_outflow_cents = net_cash_flow_cents — unlike an income statement’s expense categories, which are positive magnitudes.