Modeling
The modeling routes return derived analytics — margins, returns, growth rates, TTM aggregates — computed against a fixed point-in-time snapshot rather than against today's data. That anchoring is the point: a ratio is only reproducible if the statement versions behind it are pinned.
Every route here keys off company_id, the UUID, not the CIK. Get one from
/v1/identity/resolve.
Snapshot first
Every modeling read requires as_of, and that date must name a snapshot that
already exists. Without one the route returns 404:
{
"error": {
"code": "NOT_FOUND",
"http_status": 404,
"message": "As-of snapshot not found for modeling ratios query.",
"details": {},
"trace_id": "…"
}
}
This is the single most common surprise on these routes. A 404
here does not mean the company is uncovered or the date is invalid — it
means nobody has built a snapshot for that date yet. Build one, then read.
The modeling reads and snapshot builds are both available from Growth, so a Growth key can build the snapshot it then reads.
Build a snapshot
POST /v1/modeling/universe/pit-snapshot computes the snapshot and returns the
metric rows it produced. It takes up to 50 CIKs at a time:
curl -X POST "https://api.arche.fi/v1/modeling/universe/pit-snapshot" \
-H "X-Api-Key: $ARCHE_API_KEY" \
-H "X-Request-ID: $(uuidgen)" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"ciks": ["0000320193", "0000789019"],
"as_of_date": "2025-06-30",
"statement_types": ["INCOME_STATEMENT"],
"metrics_bundle": "tier1"
}'
All four fields are required. metrics_bundle is all or tier1.
{
"page": 1,
"page_size": 50,
"total": 12,
"items": [
{
"cik": "0000320193",
"statement_type": "INCOME_STATEMENT",
"metric_name": "BASIC_EPS",
"metric_value": "1.650000",
"statement_date": "2025-03-29",
"fiscal_year": 2025,
"fiscal_period": "Q2",
"_meta": {
"snapshot_id": "2128682cf89d4e3a364918c482ca249c2d888b066de903f559bb67cf0b0e48cb",
"as_of_snapshot_id": "48e9af60-bab1-4fb5-9027-704bdfc9e0e6",
"snapshot_hash": "26f836b84110370870b2a9239d058669f816c3fefc09431e329569508ea48156",
"statement_version_id": "003c8265-6a73-4a41-bf80-3b78e7d8ddca",
"strategy_id": "accepted_at_fallback_filed_at_eod_utc_source_preferred_own_period_acceptance_ordered_v4"
}
}
]
}
A large build can answer 202 instead of 200, meaning the snapshot is still
being computed. Retry the read rather than the build.
Every row carries the statement_version_id it came from and the
strategy_id that selected it, so a figure in a model traces back to a specific
filed statement. Record as_of_snapshot_id alongside your model run — it is
what makes the run reproducible.
Model periods
The normalized per-period rows the other routes are computed from. This is the route to read when you want the inputs rather than the derived figures:
curl -X GET "https://api.arche.fi/v1/modeling/companies/a2cb31b5-dc0f-4a17-a665-e41f59cb8e0a/periods?as_of=2025-06-30&page_size=50" \
-H "X-Api-Key: $ARCHE_API_KEY" \
-H "X-Request-ID: $(uuidgen)" \
-H "Accept: application/json"
Each row names its statement identity, its version_sequence, the
statement_version_id behind it, and normalization_provenance_id — so you can
carry a model input back to its provenance.
/v1/modeling/companies/{company_id}/multi-year returns the same rows arranged
for multi-year comparison, with the valuation inputs computed from each one.
A period row holds one statement, so an income-statement row has no balance
sheet inputs; each null input names why in unavailable_metrics.
Ratios
Margins, returns and capital-structure ratios for each period in the snapshot:
curl -X GET "https://api.arche.fi/v1/modeling/companies/a2cb31b5-dc0f-4a17-a665-e41f59cb8e0a/ratios?as_of=2025-06-30&page_size=50" \
-H "X-Api-Key: $ARCHE_API_KEY" \
-H "X-Request-ID: $(uuidgen)" \
-H "Accept: application/json"
Representative response:
{
"page": 1,
"page_size": 50,
"total": 67,
"items": [
{
"model_ratio_id": "8baeabfd-6ef4-4e07-952a-73e0f8b64dbe",
"company_id": "a2cb31b5-dc0f-4a17-a665-e41f59cb8e0a",
"cik": "0000320193",
"statement_type": "INCOME_STATEMENT",
"accounting_standard": "US_GAAP",
"statement_date": "2025-03-29",
"fiscal_year": 2025,
"fiscal_period": "Q2",
"currency": "USD",
"version_sequence": 6,
"as_of_snapshot_id": "48e9af60-bab1-4fb5-9027-704bdfc9e0e6",
"as_of_timestamp": "2025-06-30T23:59:59Z",
"snapshot_hash": "26f836b84110370870b2a9239d058669f816c3fefc09431e329569508ea48156",
"metrics": {
"GROSS_MARGIN": "0.470506",
"OPERATING_MARGIN": "0.310291",
"NET_MARGIN": "0.259860",
"ROE": "0.370980",
"ROA": "0.074811",
"ROIC": "0.191197",
"DEBT_TO_EQUITY": "1.380382",
"CURRENT_RATIO": "0.820870",
"QUICK_RATIO": "0.516245",
"BOOK_VALUE_PER_SHARE": "4.436465",
"DSO": "24.667205",
"DPO": "96.477462",
"INVENTORY_TURNOVER": "8.054235",
"CASH_CONVERSION_CYCLE": "-60.636012"
},
"unavailable_metrics": [
{
"metric": "INTEREST_COVERAGE",
"reason": "MISSING_INPUT",
"missing_inputs": ["INTEREST_EXPENSE"]
}
]
}
]
}
Ratios live in metrics as decimal strings, not floats — "0.470506" is a
47.05% gross margin. Keep them as strings or decimals; parsing to binary
floating point is how a reported figure stops reconciling.
Ratio names such as GROSS_MARGIN and ROE exist
only here. They are derived, so they are not canonical
statement metrics and /v1/coverage/metrics/GROSS_MARGIN reports
zeros for them. See Metric catalog for which names
live where.
Growth
Year-over-year, quarter-over-quarter and CAGR figures, with cagr_years naming
the window:
curl -X GET "https://api.arche.fi/v1/modeling/companies/a2cb31b5-dc0f-4a17-a665-e41f59cb8e0a/growth?as_of=2025-06-30&page_size=50" \
-H "X-Api-Key: $ARCHE_API_KEY" \
-H "X-Request-ID: $(uuidgen)" \
-H "Accept: application/json"
Rows carry REVENUE_GROWTH_YOY, REVENUE_GROWTH_QOQ, NET_INCOME_GROWTH_YOY
and similar in metrics, in the same decimal-string form.
Trailing twelve months
TTM aggregates, with the window stated on every row so a figure is never ambiguous about what it sums:
curl -X GET "https://api.arche.fi/v1/modeling/companies/a2cb31b5-dc0f-4a17-a665-e41f59cb8e0a/ttm?as_of=2025-06-30&page_size=50" \
-H "X-Api-Key: $ARCHE_API_KEY" \
-H "X-Request-ID: $(uuidgen)" \
-H "Accept: application/json"
Each row carries ttm_window_start and ttm_window_end beside the summed
metrics. Check the window before comparing two companies: a TTM figure that
ends on a different date is a different measurement.
A line is totalled only when every quarter in the window reports it. A line
some quarter omits is listed in unavailable_metrics as
INCOMPLETE_TRAILING_YEAR rather than summed over fewer than four quarters.
When a metric can't be computed
Ratio, growth and multi-year rows never leave a metric silently absent. Each
metric the route computes that a row does not carry is listed in
unavailable_metrics, with the reason and, when an unreported input is the
cause, the canonical lines that were missing. TTM rows list the lines their
window reports but cannot total. In the ratios example above,
Apple's income statement for the quarter has no interest expense line, so
interest coverage has no divisor:
"unavailable_metrics": [
{
"metric": "INTEREST_COVERAGE",
"reason": "MISSING_INPUT",
"missing_inputs": ["INTEREST_EXPENSE"]
}
]
reason is one of a fixed set, so code can branch on it:
reason | Meaning |
|---|---|
NOT_REPORTED | The filing does not report the line for the period. |
NOT_ON_STATEMENT | The value comes from a different statement than the row. Read that statement's row for the period. |
MISSING_INPUT | An input the formula needs is not reported for the period. missing_inputs names it. |
DENOMINATOR_ZERO | Every input is present, but the divisor is zero. |
NON_POSITIVE_VALUE | The formula is defined only for positive values, such as a CAGR base or a share count, and one is zero or negative. |
NO_COMPARABLE_PERIOD | A growth rate has no earlier period of the same length to compare against. |
NOT_APPLICABLE_TO_PERIOD | The metric is not defined for this kind of period, such as quarter-over-quarter growth on an annual row. |
INCOMPLETE_TRAILING_YEAR | Not every quarter of the trailing twelve months reports the line. |
PENDING_REBUILD | The inputs now support a value that the stored row predates. The next model build fills it. |
On ratios and growth, every metric in the route's set is in metrics or in
unavailable_metrics. On multi-year, every null in valuation_inputs has an
entry. Model periods carry the statement as reported, so a line absent from
their metrics was not reported.
Coverage varies by company
A snapshot covering a company does not guarantee every derived table has rows for it. In the same snapshot above, Apple returns 67 ratio rows and Microsoft returns none, while both return model periods.
An empty items with total: 0 here means the derived rows were not computed
for that company, not that the company is uncovered. Read
/v1/modeling/companies/{company_id}/periods to confirm the inputs exist.
Within a row that does exist, a missing ratio is never zero: its reason is in
unavailable_metrics.